Repository: octave-online/octave-online-server Branch: master Commit: dea7292195bc Files: 321 Total size: 1.8 MB Directory structure: gitextract_lv487tfw/ ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gcloudignore ├── .github/ │ └── workflows/ │ └── build-test.yml ├── .gitignore ├── .npmrc ├── COPYING ├── Makefile ├── README.md ├── back-filesystem/ │ ├── .eslintrc.yml │ ├── .npmrc │ ├── README.md │ ├── app.js │ ├── git/ │ │ ├── create-repo-service.js │ │ ├── create-repo.service │ │ └── gitd.service │ ├── package.json │ └── src/ │ ├── controller.js │ ├── fake-socket.js │ ├── git-util.js │ ├── logger.js │ ├── mime.types │ └── working-util.js ├── back-master/ │ ├── .eslintrc.yml │ ├── .npmrc │ ├── README.md │ ├── app.js │ ├── package.json │ └── src/ │ ├── capped-file-system.js │ ├── docker-handler.js │ ├── main-flavor.js │ ├── main-pool.js │ ├── maintenance-request-manager.js │ ├── maintenance.js │ ├── message-translator.js │ ├── octave-session.js │ ├── process-handler.js │ ├── session-impl.js │ └── session-manager.js ├── back-octave/ │ ├── Makefile │ ├── README.md │ ├── host.c │ └── oo-changesets/ │ ├── 000-README.md │ ├── 001-d38b7c534496.hg.txt │ ├── 002-d3de6023e846.hg.txt │ ├── 003-4d28376c34a8.hg.txt │ ├── 004-6ff3e34eea77.hg.txt │ ├── 005-9e73fe0d92d5.hg.txt │ ├── 006-15d21ceec728.hg.txt │ ├── 007-4d778d6ebbd0.hg.txt │ ├── 008-e8ef7f3333bf.hg.txt │ ├── 009-05f7272c001e.hg.txt │ ├── 010-4a1afb661c55.hg.txt │ ├── 011-7327936fa23e.hg.txt │ ├── 012-84390db50239.hg.txt │ ├── 013-f4110d638cdb.hg.txt │ ├── 014-21fd506b7530.hg.txt │ ├── 100-2d1fd5fdd1d5.hg.txt │ ├── 100-README.md │ ├── 101-bc8cd93feec5.hg.txt │ ├── 102-30d8ba0fbc32.hg.txt │ ├── 103-352b599bc533.hg.txt │ ├── 104-9475120a3110.hg.txt │ ├── 105-ccbef5c9b050.hg.txt │ ├── 106-91cb270ffac0.hg.txt │ ├── 107-80081f9d8ff7.hg.txt │ ├── 108-9b39ca8bcbfd.hg.txt │ ├── 200-84cbf166497f.hg.txt │ ├── 200-README.md │ ├── 201-b993253f19d0.hg.txt │ ├── 202-d9d23f97ba78.hg.txt │ ├── 203-d6b5ffb8e4cc.hg.txt │ ├── 204-e61d7b8918e2.hg.txt │ ├── 300-d78448f9c483.hg.txt │ ├── 301-97f7d1f4fe83.hg.txt │ ├── 302-8900d7cf8554.hg.txt │ ├── 310-1e1c91e6cddc.hg.txt │ ├── 320-8d4683a83238.hg.txt │ ├── 321-faad58416a3a.hg.txt │ ├── 400-7ade2492e023.hg.txt │ ├── 401-1b33dc797ec9.hg.txt │ ├── 402-b01fa2864d4d.hg.txt │ ├── 403-2813cb96e10f.hg.txt │ ├── 404-acb523f25bb9.hg.txt │ ├── 405-6ad34b0b69e1.hg.txt │ ├── 406-d0df6f16f41e.hg.txt │ ├── 407-df206dd11399.hg.txt │ ├── 408-8184a51579f3.hg.txt │ ├── 420-4c3d80dd9e65.hg.txt │ ├── 421-de16dd99ab0e.hg.txt │ ├── 422-de16dd99ab0e.hg.txt │ └── 430-d2250ae9bddd.hg.patch ├── client/ │ ├── .bowerrc │ ├── .gitignore │ ├── .npmrc │ ├── COPYING │ ├── Gruntfile.js │ ├── README.md │ ├── app/ │ │ ├── .eslintrc.yml │ │ ├── colab.html │ │ ├── eula.txt │ │ ├── fonts/ │ │ │ └── dejavusansmono_book/ │ │ │ ├── DejaVuSansMono-demo.html │ │ │ ├── specimen_files/ │ │ │ │ ├── easytabs.js │ │ │ │ ├── grid_12-825-55-15.css │ │ │ │ └── specimen_stylesheet.css │ │ │ └── stylesheet.css │ │ ├── images/ │ │ │ ├── flaticons/ │ │ │ │ └── download-svg.ai │ │ │ ├── logo_collections/ │ │ │ │ ├── official/ │ │ │ │ │ └── favicon_package/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── browserconfig.xml │ │ │ │ │ └── site.webmanifest │ │ │ │ └── server/ │ │ │ │ └── favicon_package/ │ │ │ │ ├── README.md │ │ │ │ ├── browserconfig.xml │ │ │ │ └── site.webmanifest │ │ │ └── sanscons/ │ │ │ └── license.txt │ │ ├── js/ │ │ │ ├── ace-adapter.js │ │ │ ├── ace-extras.js │ │ │ ├── anal.js │ │ │ ├── app.js │ │ │ ├── base64-toBlob.js │ │ │ ├── base64v1.module.js │ │ │ ├── bucket.js │ │ │ ├── client.js │ │ │ ├── detectmobilebrowser.js │ │ │ ├── download.js │ │ │ ├── flex-resize.js │ │ │ ├── ko-ace.js │ │ │ ├── ko-flash.js │ │ │ ├── ko-takeArray.js │ │ │ ├── modernizr-201406b.js │ │ │ ├── octfile.js │ │ │ ├── onboarding.js │ │ │ ├── ot-client.js │ │ │ ├── ot-handler.js │ │ │ ├── polyfill.js │ │ │ ├── runtime.js │ │ │ ├── utils.js │ │ │ ├── vars.js │ │ │ └── ws-shared.js │ │ ├── main.js │ │ ├── privacy.txt │ │ ├── privacy_standalone.txt │ │ └── styl/ │ │ ├── all.styl │ │ ├── callouts.styl │ │ ├── editor.styl │ │ ├── flexbox.styl │ │ ├── hamburger.styl │ │ ├── header.styl │ │ ├── mixins.styl │ │ ├── modals.styl │ │ ├── output_panel.styl │ │ ├── print.styl │ │ └── themes/ │ │ ├── official/ │ │ │ ├── fire.styl │ │ │ ├── ice.styl │ │ │ ├── lava.styl │ │ │ └── sun.styl │ │ └── server/ │ │ ├── fire.styl │ │ ├── ice.styl │ │ ├── lava.styl │ │ └── sun.styl │ ├── bower.json │ └── package.json ├── config.sample.hjson ├── config_defaults.hjson ├── containers/ │ ├── README.md │ ├── octave-deps/ │ │ ├── Dockerfile │ │ └── cloudbuild.yaml │ ├── octave-oo/ │ │ ├── Dockerfile │ │ ├── cloudbuild.yaml │ │ └── java.opts │ ├── octave-pkg/ │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── cloudbuild.yaml │ │ ├── ltfat.patch │ │ └── octaverc.m │ ├── octave-stable/ │ │ ├── Dockerfile │ │ └── cloudbuild.yaml │ ├── oo-back/ │ │ ├── Dockerfile │ │ └── cloudbuild.yaml │ ├── oo-front/ │ │ ├── Dockerfile │ │ └── cloudbuild.yaml │ ├── oo-gith/ │ │ ├── Dockerfile │ │ ├── cloudbuild.yaml │ │ ├── nginx.conf │ │ └── supervisord.conf │ ├── oo-redirect/ │ │ └── cloudbuild.yaml │ ├── oos-quick-start/ │ │ └── docker-compose.yaml │ ├── utils-admin/ │ │ └── cloudbuild.yaml │ ├── utils-gitd/ │ │ ├── Dockerfile │ │ ├── cloudbuild.yaml │ │ └── supervisord.conf │ └── utils-gith/ │ ├── Dockerfile │ ├── cloudbuild.yaml │ └── supervisord.conf ├── entrypoint/ │ ├── .eslintrc.yml │ ├── README.md │ ├── back-selinux.js │ ├── exit.js.sample │ ├── oo-front.service │ ├── oo-no-restart.service │ ├── oo-reinstall.service │ ├── oo.service │ └── policy/ │ ├── octave_online.fc │ ├── octave_online.if │ ├── octave_online.te │ ├── octave_online_supplement.if │ └── octave_online_supplement.te ├── front/ │ ├── .eslintrc.yml │ ├── .npmrc │ ├── README.md │ ├── locales/ │ │ ├── README.md │ │ ├── en.yaml │ │ └── qqq.yaml │ ├── package.json │ ├── src/ │ │ ├── app.ts │ │ ├── back_server_handler.ts │ │ ├── bucket_model.ts │ │ ├── email.ts │ │ ├── express_setup.ts │ │ ├── flavor_record_model.ts │ │ ├── mongo.ts │ │ ├── octave_session_helper.ts │ │ ├── ot_document.ts │ │ ├── passport_setup.ts │ │ ├── patreon.ts │ │ ├── program_model.ts │ │ ├── session_middleware.ts │ │ ├── shared_wrap.ts │ │ ├── socket_connect.ts │ │ ├── socketio.ts │ │ ├── user_model.ts │ │ ├── utils.ts │ │ ├── views/ │ │ │ ├── captcha_error.ejs │ │ │ ├── incorrect_page.ejs │ │ │ ├── index.ejs │ │ │ ├── login_error.ejs │ │ │ ├── partials/ │ │ │ │ ├── foot.ejs │ │ │ │ └── head.ejs │ │ │ ├── patreon_link_error.ejs │ │ │ └── token_page.ejs │ │ ├── workspace_normal.ts │ │ └── workspace_shared.ts │ ├── tsconfig.json │ └── typings/ │ ├── easy-no-password.d.ts │ ├── i18next-fs-backend.d.ts │ ├── i18next-http-middleware.d.ts │ ├── ot.d.ts │ ├── pseudo-localization.d.ts │ ├── socketio-file-upload.d.ts │ └── socketio-wildcard.d.ts ├── package.json ├── redirect/ │ ├── README.md │ ├── app.js │ ├── bin/ │ │ └── server.js │ ├── package.json │ ├── src/ │ │ └── db.js │ └── views/ │ └── error.ejs ├── shared/ │ ├── .eslintrc.yml │ ├── .npmrc │ ├── README.md │ ├── async-cache.js │ ├── config-helper.js │ ├── config.js │ ├── gcp/ │ │ ├── .npmrc │ │ ├── fetch_translations.js │ │ ├── index.js │ │ ├── package.json │ │ └── reboot_or_remove_self.js │ ├── gitarchive.js │ ├── hostname.js │ ├── index.d.ts │ ├── index.js │ ├── json-stream-safe.js │ ├── logger.js │ ├── lua/ │ │ ├── get-sesscode.lua │ │ ├── ot.lua │ │ ├── ot_apply.lua │ │ ├── ot_set.lua │ │ └── ot_test.lua │ ├── metrics.js │ ├── once-message.js │ ├── online-offline.js │ ├── package.json │ ├── queue.js │ ├── redis-messenger.js │ ├── redis-queue.js │ ├── redis-util.js │ ├── silent.js │ ├── stackdriver/ │ │ ├── index.js │ │ └── package.json │ ├── stdio-messenger.js │ └── time-limit.js ├── test/ │ ├── package.json │ └── small-unit.js ├── utils-admin/ │ ├── .npmrc │ ├── README.md │ ├── app.js │ ├── bin/ │ │ ├── repo-cleanup.js │ │ └── server.js │ ├── package.json │ ├── public/ │ │ └── stylesheets/ │ │ └── style.css │ ├── routes/ │ │ ├── index.js │ │ └── users.js │ ├── src/ │ │ ├── db.js │ │ └── repo.js │ └── views/ │ ├── error.ejs │ ├── find.ejs │ ├── index.ejs │ ├── partials/ │ │ ├── footer.ejs │ │ └── header.ejs │ ├── user-list.ejs │ └── user.ejs └── utils-auth/ ├── .eslintrc.yml ├── .npmrc ├── README.md ├── app.js ├── configs/ │ ├── custom_4xx.html │ ├── gitlist.ini │ └── oo-utils-auth.service └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git/ **/_old **/node_modules **/tmp **/vendor ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 # Default to tab indentation (narrow 2-column size) indent_style = tab indent_size = 2 # Specific exceptions using space indentation [*.{json,yml}] indent_style = space indent_size = 2 ================================================ FILE: .eslintignore ================================================ node_modules/ node_modules_1/ vendor/ dist/ build/ client/app/fonts/ _old/ front/ front2/ *.d.ts ================================================ FILE: .eslintrc.yml ================================================ env: es6: true node: true extends: 'eslint:recommended' parserOptions: ecmaVersion: 2018 rules: indent: - error - tab - SwitchCase: 1 linebreak-style: - error - unix quotes: - error - double semi: - error - always ================================================ FILE: .gcloudignore ================================================ #!include:.gitignore .git/ ================================================ FILE: .github/workflows/build-test.yml ================================================ name: Node.js CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 10.x - name: Install run: | touch config.hjson npm ci (cd test && npm ci) - name: Test run: | (cd test && npm test) env: CI: true - name: Lint run: npm run lint ================================================ FILE: .gitignore ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . # Directory names that should not be tracked _old/ bundle/ dist/ node_modules/ node_modules_1/ vendor/ tmp/ front/static/ # File extensions that should not be tracked *.bak *.log *.o *.out *.pem *.pub *.pp # Specific files to omit .env config.hjson back-octave/octave-host back-octave/bin/cwd/ entrypoint/exit.js entrypoint/front_setup.js ================================================ FILE: .npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: COPYING ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: Makefile ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . SHELL := /bin/bash NODE = node # Read options from config file # \n is replaced with \1 because gnumake removes \n from ${shell}. See: https://stackoverflow.com/q/54067438/1407170 get_config = ${shell $(NODE) -e "console.log(require('./shared').config.$(1))" | tr '\n' '\1'} GIT_HOST = $(call get_config,git.hostname) GIT_DIR = $(call get_config,docker.gitdir) WORK_DIR = $(call get_config,docker.cwd) OCTAVE_SUFFIX = $(call get_config,docker.images.octaveSuffix) FILES_SUFFIX = $(call get_config,docker.images.filesystemSuffix) JSON_MAX_LEN = $(call get_config,session.jsonMaxMessageLength) CGROUP_CONF = $(call get_config,selinux.cgroup.conf) install-selinux-policy: # yum install -y selinux-policy-devel policycoreutils-sandbox selinux-policy-sandbox cd entrypoint/policy && make -f /usr/share/selinux/devel/Makefile octave_online.pp semodule -i entrypoint/policy/octave_online.pp restorecon -R -v /usr/local/lib/octave restorecon -R -v /tmp restorecon -R -v /srv/oo setenforce enforcing echo "For maximum security, make sure to put SELinux in enforcing mode by default in /etc/selinux/config." install-selinux-bin: cp entrypoint/back-selinux.js /usr/local/bin/oo-back-selinux # TODO: Put in /etc instead of /usr/lib? cp entrypoint/oo.service /usr/lib/systemd/system/oo.service systemctl daemon-reload echo "$(CGROUP_CONF)" | tr '\1' '\n' > /etc/cgconfig.d/oo.conf echo "Run `systemctl restart cgconfig.service` to load changes to cgroup configurations." systemctl enable cgconfig ln -sf $$PWD /usr/local/share/oo install-site-m: echo "This command is no longer supported. See containers/octave-pkg/Dockerfile." enable-graceful-shutdown: cp entrypoint/oo-no-restart.service /usr/lib/systemd/system/oo.service; systemctl daemon-reload; echo "Tip 1: Consider removing entrypoint/exit.js if it could cause disruption"; echo "Tip 2: Consider sending SIGUSR1 to the server process to start a graceful shutdown"; lint: cd back-filesystem && npm run lint cd back-master && npm run lint cd shared && npm run lint cd utils-auth && npm run lint clean: if [[ -e bundle ]]; then rm -rf bundle; fi ================================================ FILE: README.md ================================================ Octave Online Server ==================== This repository contains the full stack of code to run Octave Online Server, the infrastructure that powers [Octave Online](https://octave-online.net). [![Node.js CI](https://github.com/octave-online/octave-online-server/workflows/Node.js%20CI/badge.svg)](https://github.com/octave-online/octave-online-server/actions) ## High-Level Overview There are three separate components of Octave Online Server: 1. **Client**: Code that runs in the browser. 2. **Front Server**: Authentication, client session handling. 3. **Back Server**: File I/O, Octave process handling. *Communication:* The Client and Front Server communicate primarily with WebSockets via [socket.io](https://socket.io); the Front Server and Back Server communicate primarily with [Redis PubSub](https://redis.io/topics/pubsub). User account information is stored in [MongoDB](https://www.mongodb.com) and is accessed primarily from the Front Server. User files are stored in [Git on the Server](https://git-scm.com/book/en/v1/Git-on-the-Server) and are accessed primarily from the Back Server. *Scaling:* Front Servers and Back Servers can be scaled independently (in general, you need more Back Servers than Front Servers). It is also possible to run both the Front Server and the Back Server on the same computer. *Languages:* All code is written with JavaScript technologies, although for historical reasons, the three components use different flavors of JavaScript. The Client uses ES5; the Front Server uses TypeScript; and the Back Server uses ES6. ## Quick Start Read [containers/README.md](containers/README.md) for details on running a containerized version of all of Octave Online Server for use with trusted users. This is the fastest way to get off the ground. ## Manual Installation *Note:* Octave Online Server has a lot of moving parts. It is recommended that you feel comfortable with basic system administration before attempting an installation. For more details on operating each of the three components, see the respective README files: - [back-master/README.md](back-master/README.md) (back server) - [front/README.md](front/README.md) (front server) - [client/README.md](client/README.md) (client) There are also a few more directories for other components: - [back-filesystem/README.md](back-filesystem/README.md) for filesystem I/O on the back server - [back-octave/README.md](back-octave/README.md) for GNU Octave bindings for the back server - [entrypoint/README.md](entrypoint/README.md) for helper scripts to run Octave Online Server - [shared/README.md](shared/README.md) for code shared by multiple components - [utils-admin/README.md](utils-admin/README.md) for an optional admin panel - [utils-auth/README.md](utils-auth/README.md) for an optional standalone user authentication service Every subdirectory of the top-level Octave Online Server directory has a README file that explains what the contents of the directory is for. ### Prerequisites [Required] *Operating System:* Octave Online Server is built and tested exclusively on GNU/Linux. It is recommended that you use CentOS 8, although other modern distributions should work also. Most of Octave Online Server should work on macOS, but this has not been tested. [Required] *Node.js:* Octave Online Server is built and tested with Node.js LTS version 10. This is the default version on CentOS 8. # Install Node.js 10.x LTS on CentOS 8: $ sudo yum install nodejs [Required] *Redis:* Install and run a local Redis instance. Enable expiration events in `redis.conf`: $ sudo yum install redis $ sudo emacs redis.conf # Search for "notify-keyspace-events" # Set the value to "Ex" Although it is possible to use a third-party hosted Redis instance, this is not recommended because Redis latency is amplified due to its central role in the Octave Online Server architecture. [Recommended] *Git Server:* In order to persist user files between sessions, you need to set up a Git file server. It boils down to a server, which could be the current server, with a low-privileged user usually named "git". For more information, see [Git on the Server](https://git-scm.com/book/en/v1/Git-on-the-Server). Also see [back-filesystem/README.md](back-filesystem/README.md) for instructions on how to configure a Git file server for Octave Online Server. [Recommended] *MongoDB:* Install and run a MongoDB instance. Unlike Redis, MongoDB is not as central of a piece in the infrastructure, so it is possible to use a remotely hosted MongoDB if you do not want to host it locally. My experience is that it takes some time to correctly configure a fast and secure MongoDB installation. Keep in mind that MongoDB will contain personally identifiable information for user accounts. [Recommended] *Email SaaS:* If you want Octave Online Server to be able to send transactional emails, such as for email-based login, you need a [Mailgun](https://www.mailgun.com) or [Postmark](https://postmarkapp.com) account. Mailgun has a free tier that should cover most experimental and low-traffic usage. [Recommended] *ReCAPTCHA:* Certain actions, such as when email is sent, require a CAPTCHA to prevent abuse. You should register for a [ReCAPTCHA](https://www.google.com/recaptcha/) v2 Checkbox and put your credentials into your config.hjson file. [Optional] *Google Analytics:* For aggregated statistics about traffic to your site, you can enable [Google Analytics](https://www.google.com/analytics/) integration. [Optional] *Nginx:* For better performance with serving static files and easier HTTPS setup, I recommend installing and configuring [Nginx](https://www.nginx.com). However, this is not an essential piece, and it can be done after the rest of the infrastructure is up and running. ### Configuration File Read `config_defaults.hjson` to learn more about the array of settings available for Octave Online Server. When ready, copy `config.sample.hjson` into `config.hjson`, and fill in the required details. Your own `config.hjson` is ignored by source control. ### Installing Depencencies and Building In each of the five directories containing Node.js projects, go in and run `npm install`: $ (cd shared && npm install) $ (cd back-filesystem && npm install) $ (cd back-master && npm install) $ (cd front && npm install) $ (cd client && npm install) You also need to install the Bower (client-side) dependencies for the client project: $ (cd client && npm run bower install) Finally, build the client and front server projects (the back server runs without needing to be built): $ (cd client && npm run grunt) $ (cd front && npm run grunt) ### Configuring GNU Octave Octave Online Server requires a special version of GNU Octave, which needs to be built. *This is a required step.* For more information, see [back-master/README.md](back-master/README.md). ### Running Octave Online Server To run the code manually, just open up two terminals and run each of the following two commands: $ (cd back-master && DEBUG=* node app.js) $ (cd front && node app.js) To run the code as a service, you can install the systemd service provided in this repository and enable the code to be automatically run at startup; see *entrypoint/oo.service* and `make install-selinux-bin`. **Tip:** When debugging, you can modify your hosts file (on macOS, /private/etc/hosts) to create a stable URL that you can add to your Google developer console to allow Google services to work. ## Contributing You are welcome to send pull requests for consideration for addition to Octave Online Server. Pull requests are not guaranteed to be accepted; if in doubt, you should open an issue to discuss your idea before spending time writing your pull request. ### Contributor License Agreement Like many projects distributed with copyleft licenses such as AGPL, contributors to Octave Online Server must sign a Contributor License Agreement (CLA). The terms of the [Octave Online CLA](https://cla-assistant.io/octave-online/octave-online-server) are taken from [The Apache Software Foundation CLA](https://www.apache.org/licenses/contributor-agreements.html). Having a CLA in place enables Octave Online Server to be distributed with alternate licensing schemes, including commercial licenses that help keep the project afloat. ### Style If in doubt on style, follow the convention of the file you are editing. **Wrapping and Indentation:** Use tab indentation, unless in a file format such as *.yml* that requires space indentation. There is no limit on line length. This gives you full control to configure your editor to your desired width and tab size. **Naming:** In general, use camelCase for variable names and MACRO_CASE for constants. Prefix private members with an underscore (`_`). **Quotes:** Use double-quoted strings, unless you are in a context where you need a different quotation style, such as backtick strings in JavaScript. **Internationalization (i18n/l10n):** If possible, all new UI strings should be extracted into *en.yaml* so that they can be translated. For more details, see [front/locales/README.md](front/locales/README.md). **Upstream/Downstream:** Throughout the code, there are comments and function names indicating "upstream" and "downstream". "Upstream", or "U", means toward the Octave process, away from the client. "Downstream", or "D", means toward the client, away from the Octave process. So, for example, a message sent from the back server to the front server is considered downstream, and a message sent from the back server to the Octave process is considered upstream. **ECMAScript Versions:** JavaScript code in the *client* project should conform to the ECMAScript 5 standard, in order to have broad browser support. JavaScript in all other projects can use the latest ECMAScript standard supported by Node.js 6.x LTS. By design, all JavaScript code in Octave Online Server server should be able to be run natively without transcompilation to a different ECMAScript version. ### Linting The *eslint* tool will catch most style and compatibility issues in JavaScript files. Execute `npm run lint` in the top-level directory to check for errors. If your code does not pass *eslint*, you will also trigger a Travis failure on GitHub. ### Manual Testing Due to the complexity of Octave Online Server, there is not currently an automated test suite. As a contributor, you are expected to perform some manual testing to ensure that your feature does not accidentally break something else in Octave Online Server. Here are some critical user journeys that test a fairly wide cross-section of the code base. **Please make sure that all of these journeys continue working after your change.** 1. The core file editor 1. Sign in if necessary 1. Create a new file 1. Open the new file in the editor and make some changes 1. The file should appear dirty (unsaved): its name should be italic and underlined 1. Save the file; it should no longer appear dirty 1. Press the "Refresh Files" button; your file should go away and reappear a few seconds later with the same changes you had made 1. Click the following the buttons in the file toolbar, and make sure they behave as expected: - "Download File" - "Print File" - "Toggle Word Wrap" - "Save File" (make some changes first) - "Run Script" 1. Create a file named `.octaverc` with the following content: ```rcx = 5;``` 1. Run the `exit` command, then click the reconnect link 1. Once the workspace loads, check that the variable `rcx` exists and has value 5 1. Collaborative workspaces 1. Sign in if necessary 1. Open the side bar menu and enable workspace sharing if necessary 1. Open the sharing link in another window 1. Repeat all of the steps from the "core file editor" journey, mixed between the two windows, and make sure that all state gets updated as expected 1. Plotting and image processing 1. In the main command prompt, make some standard plots like `sombrero()` and `fplot(@sin, [-pi pi])`, and ensure they appear as expected 1. Open the plot window. You should be able to scroll through your plots. Ensure that the two download buttons work as expected (download as PNG and as SVG) 1. Sign in if necessary 1. Download a full-color image from [PNGNQ](http://pngnq.sourceforge.net/pngnqsamples.html); I usually use mandrill.png 1. Drag the PNG file onto the file list until it turns yellow; drop the file to upload it 1. Select the file in the list; make sure "Download File" and "Rename File" work 1. Click the "DELETE File" button to delete the file 1. Upload the file again, this time using the "Upload file" button in the file list toolbar 1. In the command prompt, run the following command: `imshow(imread("mandrill.png"))`; you should see the full-color image appear in the console output window (there is a surprisingly large amount of code that is needed to make this happen) 1. Buckets and Projects 1. Sign in if necessary 1. Create or upload multiple files if you don't already have files in your workspace 1. Open a script file that runs by itself (not a function file) 1. Click the "Share File in new Bucket" button 1. Play around with the options, adding new files and selecting a main file 1. Click "Create Bucket" 1. Ensure that the bucket creates successfully and that the main file runs 1. Click the bucket ID in the title bar. An information panel should appear with details about the bucket. Make sure they look correct 1. Click the pencil icon to edit the shortlink; change it to "test999" 1. Go to "octav.onl/test999"; the redirect should work and the same bucket page should open 1. Click "Fork This Bucket" 1. Enter "test999" as the custom URL in the "Create Project" screen. Click "Create Project"; a second or two later, you should see an alert box saying that there is a duplicate link 1. Change the shortlink so that it contains illegal characters, like "$". Click "Create Project" again; a second or two later, you should see an alert saying that the shortlink has invalid characters 1. Close the "Customize Project" dialog box, then reopen it by clicking "Fork This Bucket" again 1. This time, click "Create Project" without changing any other settings. Your browser should refresh into the new project 1. Ensure that you can edit and save files in the project 1. Open the info panel for the project by clicking it in the title bar. You should see the bucket name under "Forked From" 1. Close the info bar, then open the side bar menu 1. Find the bucket and project you created; ensure that the timestamps is correct 1. Save the links to both the bucket and the project 1. Press the "⌫" button to delete the bucket and then the project 1. Once deleted, go back to the bucket and the project with the links you saved, and ensure that the bucket and project are actaully deleted 1. Small interpreter features 1. Run a few lines of code and then run `clc`; it should clear all output from the console window 1. Run `doc fplot`; it should produce a working link 1. Run `char(randi(256, 1000, 1)' .- 1)`; it should print a nonsense string with a lot of replacement characters 1. Run `O = urlread("http://example.com")`; it should finish without error and print the HTML content of that page 1. Run `O = urlread("https://example.com")`; it should print the same HTML as the previous line (http vs https) 1. Run `O = urlread("http://cnn.com")`; it should print an error saying that the domain is not in the whitelist (unless you added that domain to your custom whitelist) 1. Run `ping`; you should see a response like "Ping time: 75ms" 1. Run `pause; disp("done")`; you should see a message "press enter to continue". Press enter, and then you should see "done" printed out to the console 1. Octave feature coverage 1. Run `gf`; you should see a message "Run 'pkg load communications' to use 'gf'" 1. Run `pkg load communications` and then `help gf`; you should get a help page 1. Run `audioread("dummy.wav")`; you should get an error that the file does not exist (but you should NOT get an error that says libsndfile was not installed) 1. Run `fork`; you should see a message "error: 'fork' undefined near line 1, column 1" 1. Student / instructor features 1. Create two accounts if you do not already have two accounts 1. In one account, add a string to the `instructor` field in mongodb; for example, `"test-course"` 1. Sign in to Octave Online Server using the other account 1. Run `enroll("test-course")` and follow the onscreen instructions 1. Sign out and sign into the first account, the one with the instructor field 1. Ensure that the student is listed in the menu bar 1. Sign out and back into the student account 1. Open the menu and try disabling sharing; it should deny permission 1. Run `enroll("default")` and follow the onscreen instructions 1. Open the menu and try disabling sharing again; it should work this time 1. Network connection and reconnecting to a session 1. Open your Octave Online Server as a guest user (not signed in) 1. Type `x = 5` and press Enter, followed by `x` and Enter, to ensure that the variable is set correctly 1. Terminate (Ctrl-C) your front server process and quickly restart it 1. The loading animation should appear on the browser window, and the animation should go away once the front server has finished restarting. In addition, the phrase "Connection lost. Attempting to reconnect..." should be printed to the console window. When the server reconnects, the prompt should activate 1. Type `x` and Enter; the variable should still have the value 5 1. Type `exit`; it should say "Octave Exited. Message: Shell Exited", and you should get a link that says "Click Here to Reconnect" 1. Terminate (Ctrl-C) your front server process and quickly restart it 1. The loading animation should appear on the browser window, and the animation should go away once the front server has finished restarting. However, you should NOT get the "Connection lost" message printed to the console, and you should NOT get an active prompt automatically after the animation goes away 1. Press the "Click Here to Reconnect" button; you should now get an active command prompt. Run a command or two to make sure the session is working normally 1. For an exhaustive test, repeate this section as (i) a signed-in user, (ii) a session with sharing enabled, and (iii) a bucket session. 1. Reconnecting to and expiring collaborative workspaces 1. Sign in to a user that has sharing enabled 1. *Ensure that no one else is viewing the user's workspace* (for example, there should be no red cursors at the command prompt) 1. Set a variable like `x = 99` 1. Reload the browser window; it should be the same session. Check that `x` is still `99` 1. Close the browser window without exiting explicitly 1. Wait for `config.redis.expire.timeout` milliseconds to ellapse, then open up a new tab for that user; it should be a new session. Check that `x` is no longer set to `99` 1. GUI: Flexbox panels and CSS 1. Hover over the border between panels; a slider should appear. Drag the slider around to resize the panels 1. Open the menu and click "Change/Reset Layout"; the panel sizes should reset to the defaults 1. Open the menu and click "Change Theme"; you should get a dark theme. Clicking the button again should change the theme back 1. GUI: Function arguments and filenames 1. Run the command `edit demo_fn.m`; it should create a new file with that name and open it in the editor 1. Enter the following content for that file: ``` function [o] = demo_fn(x) o = x*2; endfunction ``` 1. Click the "Run" button. You should get a prompt asking you for the value of x. Enter a value such as 3. You should now see `ans = 6` in the console output window 1. Press Command+R or Control+R. The same prompt should appear 1. Attempt to create another new file with the same name, `demo_fn.m`, using the "Create empty file" button. You should not be able to create a file with that name since it already exists 1. GUI: Command prompt features 1. Type `fpl` into the prompt box, then hit TAB. You should get a menu of auto-completions like `fplot` 1. Run several commands, such as `x=1` then `x=2` then `x=3`. Press the up arrow. You should be able to scroll through your command history 1. You should see "x" in the Vars menu. Click on the x. A dialog should open telling you the current value of x 1. Within the command output panel, click on command text, to the right of the "octave:#>". That command should appear in the URL bar 1. Reload the page. The command you clicked (the one now in the URL) should be automatically executed after the page loads 1. GUI: Legal and account management 1. Open the side bar menu. Click on "Privacy Policy and EULA". A dialog should open showing that content 1. Make sure you are signed in 1. Click "Change Password". Follow the instructions to change the password 1. Sign out and sign back in using your new password to make sure it worked 1. GUI: Folders 1. Use the "Create empty file" button to create a file named "dir1/foo.m". It should create a file in that subdirectory, "dir1", shown in the file list panel 1. Enter the command `cd dir1`; you should now be changed into that directory and there should be a small window reminding you in the top left of the console output window 1. Pushing the limits: File Size 1. Make sure you are *not* signed in 1. Run the following command line; it should finish without any errors: ```A = rand(500); save A.mat; load A.mat``` 1. Run the following command line; it should produce the error "load: failed to load matrix constant", due to hitting the 20 MB file size limit per workspace: ```A = rand(5000); save A.mat; load A.mat``` 1. Pushing the limits: Message Size 1. Run the following command line; it should finish without any errors and produce a busy line plot: ```plot(rand(100));``` 1. Run the following command line; it should produce the error "Warning: Suppressed a large plot", due to hitting the 1 MB limit on message size and therefore plot size: ```plot(rand(300));``` 1. Pushing the limits: Countdown / Time Limit 1. Run the following command: ```pause(12)``` 1. When the "Add 15 Seconds" link appears, click it 1. Ensure that the time runs out after 12 seconds from the original entry of the command 1. Pushing the limits: Payload and signals 1. Run the following command: ```x = 0; while(true), x += 1, end``` 1. The variable `x` should get to somewhere between 1500 and 2000 before being paused for payload 1. Click the "Resume Execution" button, and `x` should climb by approximately the same amount 1. Click the x button to stop execution. There may be a bit more output, but you should soon be returned to the command prompt 1. Repeat the above steps, but instead of clicking the x button, wait for the payload timeout to finish on its own and return you to the command prompt Tip: A community member like you could implement an automated end-to-end test suite. If this is your area of expertise, please open an issue and engage! ## License Octave Online Server is licensed under the [GNU Affero General Public License](https://en.wikipedia.org/wiki/Affero_General_Public_License). > Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. > > Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. A copy of the license can be found in COPYING. Note: You may contact webmaster@octave-online.net to inquire about other options for licensing Octave Online Server. ================================================ FILE: back-filesystem/.eslintrc.yml ================================================ ================================================ FILE: back-filesystem/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: back-filesystem/README.md ================================================ Octave Online Server: Back Server, Filesystem Utilities ======================================================= This directory contains the source code dealing with the filesystem for the Octave Online Server back server. It also contains scripts for interacting with the Git file server. ## Git File Server The subdirectory *git* contains files for running a Git file server for Octave Online Server. *gitd.service* is a systemd service that enables the Git daemon with the repository root at */srv/oo/git*, which is expected to contain *repos* and *buckets* subdirectories that are read/write to the *git* user. *create-repo.service* is a systemd service that runs a tiny server for creating empty repositories. It has the same path expectations as *gitd.service*. It invokes the path */usr/local/bin/create-repo-service*, which is expected to be a copy of *create-repo-service.js* from this project. **CAUTION:** Neither *gitd.service* nor *create-repo.service* require any authentication. You should therefore run these services behind a firewall. ## History When using the SELinux backend, the code in this directory is run in the main event loop (Node.js process) along with the *back-master* code. When the Docker backend is used, however, this code runs inside of a Docker container, while *back-master* runs outside of a Docker container. This separation was done in order to make file permissions work correctly. However, this separation is one reason why the Docker implementation is not able to handle as many concurrent sessions as the SELinux implementation. ================================================ FILE: back-filesystem/app.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; // This is the entrypoint for a standalone version of back-filesystem, used by the Docker mode but NOT the SELinux mode. const messenger = new (require("@oo/shared").StdioMessenger)(); const log = require("@oo/shared").logger("app"); const FilesController = require("./src/controller"); // Customize options on the logger require("./src/logger"); // Read command-line arguments const GIT_DIR = process.argv[2]; const WORK_DIR = process.argv[3]; log.info("Dirs:", GIT_DIR, WORK_DIR); // Make an instance of controller var controller = new FilesController(GIT_DIR, WORK_DIR, ""); // Set up the STDIO messenger instance so we can talk to the master messenger._log = require("@oo/shared").logger("messenger"); messenger.setReadStream(process.stdin); messenger.setWriteStream(process.stdout); messenger.on("message", (name, content) => { controller.receiveMessage(name, content); }); controller.on("message", (name, content) => { messenger.sendMessage(name, content); }); messenger.on("error", (err) => { log.error("messenger:", err); }); // Send acknowledgement message downstream messenger.sendMessage("ack", true); ================================================ FILE: back-filesystem/git/create-repo-service.js ================================================ #!/usr/bin/env node /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; /* eslint-disable no-console */ const child_process = require("child_process"); const fs = require("fs"); const http = require("http"); const path = require("path"); const url = require("url"); if (process.argv.length !== 4) { console.error("Usage: node create-repo-service.js /path/to/git/root PORT"); process.exit(1); } const gitRoot = process.argv[2]; const port = parseInt(process.argv[3]); http.createServer((req, res) => { const { query } = url.parse(req.url, true); const isoTime = new Date().toISOString(); if (["buckets", "repos"].indexOf(query.type) === -1) { res.writeHead(400, "Invalid type"); console.log(`create-repo-service: ${isoTime} Invalid type`); return res.end(); } if (!query.name || !/^[\w]+$/.test(query.name)) { res.writeHead(400, "Invalid name"); console.log(`create-repo-service: ${isoTime} Invalid name`); return res.end(); } const bareRepoPath = path.join(gitRoot, query.type, query.name + ".git"); fs.stat(bareRepoPath, (err) => { const exists = !err; let process; if (query.action === "delete") { if (exists) { process = child_process.spawn("rm", ["-rf", bareRepoPath]); } else { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Already Deleted\n"); console.log(`create-repo-service: ${isoTime} Already Deleted: ${bareRepoPath}`); return; } } else { if (exists) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Already Created\n"); console.log(`create-repo-service: ${isoTime} Already Created: ${bareRepoPath}`); return; } else { process = child_process.spawn("git", ["init", "--bare", bareRepoPath]); } } let resData = Buffer.alloc(0); process.stdout.on("data", (chunk) => { resData = Buffer.concat([resData, chunk]); }); process.stderr.on("data", (chunk) => { resData = Buffer.concat([resData, chunk]); }); process.on("exit", (code /* , signal */) => { const operation = (query.action === "delete") ? "Delete" : "Init"; if (code === 0) { res.writeHead(200, { "Content-Type": "text/plain" }); res.write(`${operation} Success\n`); console.log(`create-repo-service: ${isoTime} ${operation} Success: ${bareRepoPath}`); } else { res.writeHead(500, { "Content-Type": "text/plain" }); res.write(`${operation} Error\n`); console.log(`create-repo-service: ${isoTime} ${operation} Error: ${bareRepoPath}`); console.log(`create-repo-service: ${resData.toString("utf-8")}`); } res.write(resData); res.end(); }); }); }).listen(port); console.log("create-repo-service: Listening on port", port); ================================================ FILE: back-filesystem/git/create-repo.service ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ############################################################### # NOTE: This systemd service file is here for reference only; # # it is not currently being used in Octave Online Server. # ############################################################### [Unit] Description=Service to create and delete bare git repos [Service] ExecStart=/usr/local/bin/create-repo-service /srv/oo/git 3003 Environment=NODE_ENV=production Restart=always StandardOutput=syslog StandardError=syslog User=git Group=git [Install] WantedBy=multi-user.target ================================================ FILE: back-filesystem/git/gitd.service ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ############################################################### # NOTE: This systemd service file is here for reference only; # # it is not currently being used in Octave Online Server. # ############################################################### [Unit] Description=Start Read-Write Git Daemon [Service] ExecStart=/usr/bin/git -c daemon.receivepack=true daemon --verbose --reuseaddr --export-all --base-path=/srv/oo/git /srv/oo/git Restart=always RestartSec=500ms StandardOutput=syslog StandardError=syslog SyslogIdentifier=git-daemon User=git Group=git [Install] WantedBy=multi-user.target ================================================ FILE: back-filesystem/package.json ================================================ { "name": "@oo/files", "version": "0.0.0", "description": "Reads and writes to an Octave session working directory", "main": "app.js", "scripts": {}, "author": "Octave Online LLC", "license": "AGPL-3.0", "private": true, "engines": { "node": "18.x" }, "dependencies": { "@oo/shared": "file:../shared", "async": "^1.5.2", "charset-detector": "0.0.2", "debug-logger": "^0.4.1", "iconv": "^3", "mime": "^1.3.4", "socketio-file-upload": "^0.4.4", "sprintf-js": "^1.0.3" } } ================================================ FILE: back-filesystem/src/controller.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const async = require("async"); const GitUtil = require("./git-util"); const WorkingUtil = require("./working-util"); const path = require("path"); const SocketIOFileUploadServer = require("socketio-file-upload"); const config = require("@oo/shared").config; const FakeSocket = require("./fake-socket"); const logger = require("@oo/shared").logger; const EventEmitter = require("events"); class FilesController extends EventEmitter { constructor(gitDir, workDir, logMemo) { super(); this._log = logger(`files-controller:${logMemo}`); this._mlog = logger(`files-controller:${logMemo}:minor`); this.gitUtil = new GitUtil(gitDir, logMemo); this.workingUtil = new WorkingUtil(workDir, logMemo); this.workDir = workDir; this.user = null; this.bucketId = null; this.ready = false; this.destroyed = false; this.fakeSocket = new FakeSocket(); this.fakeSocket.on("_emit", this._sendMessage.bind(this)); this._setupUploader(); } // Returns whether Git operations are safe to perform. // Check this.ready for whether file operations (without git) are safe. _isInitialized() { return this.ready && (this.user !== null || this.bucketId !== null); } // Returns whether either "user-info" or "bucket-info" has been called _isSetUp() { return this.user || this.bucketId || this.ready; } receiveMessage(name, content) { switch (name) { case "user-info": { if (this._isSetUp()) { this._log.error("user-info called, but already set up:", content); return; } this.user = content.user; if (this.user) { this._log.info("Received user:", this.user.consoleText); this._legalTime = content.legalTime; // FIXME: For backwards compatibility } else { this._log.info("No user this session"); this.ready = true; this._sendMessage("files-ready", {}); return; } async.waterfall([ (_next) => { this.gitUtil.initialize(this.user, this.workDir, _next); }, (results, _next) => { this.workingUtil.hasOctaverc(_next); }, (hasOctaverc, _next) => { this.ready = true; this._sendMessage("files-ready", { hasOctaverc }); _next(null); }, (_next) => { this.workingUtil.listAll(_next); } ], (err, fileData) => { if (err) { if (/unable to write file/.test(err.message)) { return this._fail("filelist", "warn", `Whoops! You are currently exceeding your space limit of ${config.docker.diskQuotaKiB} KiB.\nPlease open a support ticket and we will help you resolve the\nissue. Sorry for the inconvenience!`); } else { if (this._logError("initialize", err)) { this._fail("filelist", "warn", "Unable to load your files from the server: please try again."); } return; } } this._mlog.debug("User successfully initialized"); this._sendMessage("filelist", { success: true, legalTime: this._legalTime, // FIXME: for backwards compatibility files: fileData, refresh: false }); }); break; } case "bucket-info": { if (this._isSetUp()) { this._log.error("bucket-info called, but already set up:", content); return; } this.bucketId = content.id; this._legalTime = content.legalTime; // FIXME: For backwards compatibility // If content.readonly is false, this request is for a project or for creating the bucket. If content.readonly is true, this request is for reading from the bucket. this._log.info("Received bucket:", this.bucketId, content.readonly); async.waterfall([ (_next) => { this.gitUtil.initializeBucket(this.bucketId, this.workDir, content.readonly, _next); }, (results, _next) => { this.workingUtil.hasOctaverc(_next); }, (hasOctaverc, _next) => { this.ready = true; this._sendMessage("files-ready", { hasOctaverc }); _next(null); }, (_next) => { this.workingUtil.listAll(_next); } ], (err, fileData) => { if (err) return this._logError("bucket", err); this._mlog.debug("Bucket successfully initialized"); this._sendMessage("filelist", { success: true, legalTime: this._legalTime, // FIXME: for backwards compatibility files: fileData, refresh: false }); }); break; } case "list": { if (!this.ready) return this._mlog.warn("list: not ready"); this._mlog.debug("Listing files..."); async.waterfall([ (_next) => { this.workingUtil.listAll(_next); } ], (err, fileData) => { if (err) return this._logError("list", err); this._log.debug("Files successfully listed"); this._sendMessage("filelist", { success: true, legalTime: this._legalTime, // FIXME: for backwards compatibility files: fileData, refresh: false }); }); break; } case "refresh": { if (!this._isInitialized()) return this._mlog.warn("refresh: not initialized"); this._mlog.debug("Refreshing files..."); async.waterfall([ (_next) => { this.gitUtil.pullPush("Scripted user file commit", _next); }, (results, _next) => { this.workingUtil.listAll(_next); } ], (err, fileData) => { if (err) return this._logError("refresh", err); this._log.debug("Files successfully refreshed"); this._sendMessage("filelist", { success: true, files: fileData, refresh: true }); }); break; } case "commit": { if (!this._isInitialized()) return this._fail("committed", "warn", "Not initialized"); // NOTE: In a readonly repository (buckets), this is a no-op. const comment = content.comment; if (!comment) return this._fail("committed", "warn", "Empty comment:", comment); this._mlog.debug("Committing files..."); async.waterfall([ (_next) => { this.gitUtil.pullPush(comment, _next); } ], (err) => { if (err) return this._fail("committed", "warn", err); this._log.debug("Files successfully committed (except for readonly)"); return this._sendMessage("committed", { success: true }); }); break; } case "save": { if (!this.ready) return this._fail("save", "warn", "Not ready"); const filename = content.filename; const value = content.content; this._mlog.debug("Saving file:", filename); if (!filename) return this._fail("saved", "warn", "Empty file name:", filename, value); async.waterfall([ (_next) => { this.workingUtil.saveFile(filename, value, _next); } ], (err, md5sum) => { if (err) { if (/ENOSPC/.test(err.message)) return this._fail("saved", "warn", `Whoops, you reached your space limit (${config.docker.diskQuotaKiB} KiB).\nYou should free up space to ensure that changes you make get committed.\nRunning the command "system('rm octave-workspace')" might help.`); else return this._fail("saved", "error", err); } this._log.debug("File successfully saved"); return this._sendMessage("saved", { success: true, filename, md5sum }); }); break; } case "rename": { if (!this.ready) return this._fail("rename", "warn", "Not ready"); const oldname = content.filename; const newname = content.newname; if (!oldname || !newname) return this._fail("renamed", "warn", "Empty file name or new name:", oldname, newname); this._mlog.debug("Renaming file:", oldname, newname); async.waterfall([ (_next) => { this.workingUtil.renameFile(oldname, newname, _next); } ], (err) => { if (err) return this._fail("renamed", "error", err); this._log.debug("File successfully renamed"); return this._sendMessage("renamed", { oldname, newname, success: true }); }); break; } case "delete": { if (!this.ready) return this._fail("delete", "warn", "Not ready"); const filename = content.filename; if (!filename) return this._fail("deleted", "warn", "Empty file name:", filename); this._mlog.debug("Deleting file:", filename); async.waterfall([ (_next) => { this.workingUtil.deleteFile(filename, _next); } ], (err) => { if (err) { if (/ENOENT/.test(err.message)) return this._fail("deleted", "warn", `Whoops, the file ${filename} does not exist any more.\nTry pressing the "refresh files" button in the file manager toolbar.`); else return this._fail("deleted", "error", err); } this._log.debug("File successfully deleted"); return this._sendMessage("deleted", { success: true, filename }); }); break; } case "binary": { if (!this.ready) return this._mlog.warn("binary: not ready"); const filename = content.filename; if (!filename) return this._fail("binary", "warn", "Empty file name:", filename); this._mlog.debug("Loading binary file:", filename); async.waterfall([ (_next) => { this.workingUtil.readBinary(filename, _next); } ], (err, base64data, mime) => { if (err) return this._fail("binary", "error", err); this._log.debug("File successfully loaded"); return this._sendMessage("binary", { success: true, filename, base64data, mime }); }); break; } case "read-delete-binary": { if (!this.ready) return this._mlog.warn("read-delete-binary: not ready"); const filename = content.filename; if (!filename) return this._fail("deleted-binary", "warn", "Empty file name:", filename); this._mlog.debug("Loading and deleting binary file:", filename); async.series([ (_next) => { this.workingUtil.readBinary(filename, _next); }, (_next) => { this.workingUtil.deleteFile(filename, _next); } ], (err, results) => { if (err) return this._fail("deleted-binary", "error", err); const base64data = results[0][0]; const mime = results[0][1]; this._log.debug("File successfully loaded and deleted"); return this._sendMessage("deleted-binary", { success: true, filename, base64data, mime }); }); break; } case "multi-binary": { if (!this.ready) return this._mlog.warn("multi-binary: not ready"); const filenames = content.filenames; const responseName = "multi-binary:" + content.id; if (!Array.isArray(filenames)) return this._fail(responseName, "warn", "Invalid filename array:", filenames); this._mlog.debug("Loading multiple files", responseName, filenames); async.map(filenames, (filename, _next) => { async.waterfall([ (__next) => { this.workingUtil.readBinary(filename, __next); }, (base64data, mime, __next) => { __next(null, base64data); } ], _next); }, (err, results) => { if (err) return this._fail(responseName, "error", err); this._mlog.trace("Files finished loading", responseName); return this._sendMessage(responseName, { success: true, results }); }); break; } case "save-multi-binary": { if (!this.ready) return this._mlog.warn("save-multi-binary: not ready"); const filenames = content.filenames; const base64datas = content.base64datas; const responseName = "multi-binary-saved:" + content.id; if (!Array.isArray(filenames) || !Array.isArray(base64datas) || filenames.length !== base64datas.length) return this._fail(responseName, "warn", "Invalid array:", filenames, base64datas); this._mlog.debug("Writing multiple files:", responseName, filenames); async.times(filenames.length, (i, _next) => { const buffer = new Buffer(base64datas[i], "base64"); this.workingUtil.saveFile(filenames[i], buffer, _next); }, (err, results) => { if (err) return this._fail(responseName, "error", err); this._mlog.trace("Files finished writing", responseName); return this._sendMessage(responseName, { success: true, results }); }); break; } // Send remaining messages to the fakeSocket default: { this.fakeSocket.trigger(name, content); break; } } } destroy() { this.destroyed = true; } _logError(context, err) { if (this.destroyed) { this._mlog.trace("Ignoring git error:", context, err); return false; } else { this._log.error(context, err); return true; } } // Send messages downstream _sendMessage(name, content) { this.emit("message", name, content); } // Log and send failure messages _fail() { let args = Array.prototype.slice.call(arguments, 2); let messageString = args.join(" "); this._log[arguments[1]].apply(this, args); this._sendMessage(arguments[0], { success: false, message: messageString }); } // Set up SIOFU _setupUploader() { const uploader = new SocketIOFileUploadServer(); uploader.dir = this.workDir; uploader.emitChunkFail = true; uploader.on("saved", (event) => { const filename = path.basename(event.file.pathName); this.workingUtil.getFileInfo(filename, (err, fileInfo) => { if (err) return this._log.warn(err); if (!fileInfo) return this._fail("saved", "warn", "Your file uploaded, but it will not appear in the list due to an illegal file name."); this._log.debug("File successfully uploaded"); return this._sendMessage("fileadd", fileInfo); }); }); uploader.on("error", (event) => { if (/ENOSPC/.test(event.error.message)) return this._fail("saved", "debug", `Uploading ${event.file.name}:\nIf your file is large and causes you to exceed your space limit\n(${config.docker.diskQuotaKiB} KiB), the file may be incomplete.`); this._log.error("siofu:", event); }); uploader.listen(this.fakeSocket); } } module.exports = FilesController; ================================================ FILE: back-filesystem/src/fake-socket.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); // Object to replicate the API of a server-side Socket.IO socket instance. // // To trigger all local listeners with an emulated socket message, call // myFakeSocket.trigger(name,data) // // To listen for when local methods want to send a message, listen for // the "_emit" event on your FakeSocket instance. class FakeSocket extends EventEmitter { constructor() { super(); } } // Change around some of the methods const oldEmit = FakeSocket.prototype.emit; FakeSocket.prototype.trigger = oldEmit; FakeSocket.prototype.emit = function(){ const args = Array.prototype.slice.call(arguments); args.unshift("_emit"); oldEmit.apply(this, args); }; module.exports = FakeSocket; ================================================ FILE: back-filesystem/src/git-util.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const async = require("async"); const child_process = require("child_process"); const logger = require("@oo/shared").logger; const config = require("@oo/shared").config; const silent = require("@oo/shared").silent; class GitUtil { constructor(gitDir, logMemo) { this._log = logger(`git-util:${logMemo}`); this._mlog = logger(`git-util:${logMemo}:minor`); this.execOptions = { cwd: gitDir }; this.readonly = false; this._initialized = false; // TODO: Prevent multiple git operations from taking place simultaneously. } initialize(user, workDir, next) { if (this._initialized) { this._log.error("Initializing a repository for a user that was already initialized"); return; } const remote = this._userToRemote(user); async.series([ (_next) => { this._createUserOnRemote(user, _next); }, (_next) => { this._initialize(remote, workDir, _next); } ], next); } initializeBucket(bucketId, workDir, readonly, next) { if (this._initialized) { this._log.error("Initializing a repository for a bucket that was already initialized"); return; } this.readonly = readonly; const remote = this._bucketToRemote(bucketId); async.series([ (_next) => { if (!readonly) { this._createBucketOnRemote(bucketId, _next); } else { _next(null); } }, (_next) => { this._initialize(remote, workDir, _next); } ], next); } _initialize(remote, workDir, next) { async.series([ (_next) => { this._mlog.trace("Running git init..."); child_process.execFile("git", ["--git-dir=.", `--work-tree=${workDir}`, "init"], this.execOptions, _next); }, (_next) => { // May 2018: Do not log email-based Git URL const idx = remote.indexOf("_"); const safeOrigin = (idx === -1) ? remote : remote.substr(0, idx) + "_…"; this._mlog.info("Setting origin:", safeOrigin); child_process.execFile("git", ["remote", "add", "origin", remote], this.execOptions, _next); }, (_next) => { this._pull(_next); }, (_next) => { this._initialized = true; _next(null); } ], next); } pullPush(message, next) { if (!this._initialized) { // Trying to sync the repo before it has been initialized; do not attempt to push, because no local changes are possible. return next(null); } if (this.readonly) { return this._pull(next); } else { return this._pullPush(message, next); } } _pullPush(message, next) { async.series([ (_next) => { this._mlog.debug("Preparing to pull-push..."); _next(); }, (_next) => { this._commit(message, _next); }, (_next) => { // Perform a shallow clone to avoid wasting time and resources downloading old refs from the server // This command can fail silently for the case when the remote repo is empty child_process.execFile("git", ["fetch", "--depth=1"], this.execOptions, silent(/no matching remote head/, _next)); }, (_next) => { // Resolve merge conflicts by committing all the conflicts into the repository, and let the user manually fix the conflict next time the log in. // This command can fail silently for the case when origin/master does not exist const mergeArgs = config.git.supportsAllowUnrelatedHistories ? ["merge", "--no-commit", "--allow-unrelated-histories", "origin/master"] : ["merge", "--no-commit", "origin/master"]; child_process.execFile("git", this._gitConfigArgs().concat(mergeArgs), this.execOptions, silent(/fix conflicts|not something we can merge/, _next).stdout); }, (_next) => { this._commit("Scripted merge", _next); }, (_next) => { // Push the changes up // This command can fail silently for the case when the local branch "master" is empty child_process.execFile("git", ["push", "origin", "master"], this.execOptions, silent(/src refspec master does not match any/, _next)); }, (_next) => { this._mlog.debug("Finished pull-push"); _next(null); } ], next); } _pull(next) { async.series([ (_next) => { this._mlog.debug("Preparing to pull..."); _next(); }, (_next) => { // Perform a shallow clone to avoid wasting time and resources downloading old refs from the server // This command can fail silently for the case when the remote repo is empty child_process.execFile("git", ["fetch", "--depth=1"], this.execOptions, silent(/no matching remote head/, _next)); }, (_next) => { child_process.execFile("git", this._gitConfigArgs().concat(["merge", "origin/master"]), this.execOptions, silent(/not something we can merge/, _next)); }, (_next) => { this._mlog.debug("Finished pull"); _next(); } ], next); } _commit(message, next) { async.series([ (_next) => { child_process.execFile("git", ["add", "--all"], this.execOptions, _next); }, // Remove the following check since the new "capped file system" should take care of this for us // (_next) => { // // Do not commit files greater than 1MB in size // child_process.exec("find . -size +1M -type f -exec git reset {} \\;", this.execOptions, _next); // }, (_next) => { // This command can safely fail silently for the case when there are no files to commit (in that case, the error is empty) // Note that specifying --author here does not seem to work; I have to do -c ... instead child_process.execFile("git", this._gitConfigArgs().concat(["commit", "-m", message]), this.execOptions, silent(/nothing to commit/, _next).stdout); } ], next); } _createUserOnRemote(user, next) { async.series([ (_next) => { this._mlog.debug("Preparing remote repo..."); _next(); }, (_next) => { fetch(`http://${config.git.hostname}:${config.git.createRepoPort}/?` + new URLSearchParams({ type: "repos", name: user.parametrized })).then((response) => { if (!response.ok) { return _next(new Error("Not 2xx response", { cause: response })); } _next(); }).catch(_next); } ], next); } _createBucketOnRemote(bucketId, next) { async.series([ (_next) => { this._mlog.debug("Preparing remote bucket..."); _next(); }, (_next) => { fetch(`http://${config.git.hostname}:${config.git.createRepoPort}/?` + new URLSearchParams({ type: "buckets", name: bucketId })).then((response) => { if (!response.ok) { return _next(new Error("Not 2xx response", { cause: response })); } _next(); }).catch(_next); } ], next); } _userToRemote(user) { return `git://${config.git.hostname}:${config.git.gitDaemonPort}/repos/${user.parametrized}.git`; } _bucketToRemote(bucketId) { return `git://${config.git.hostname}:${config.git.gitDaemonPort}/buckets/${bucketId}.git`; } _gitConfigArgs() { return ["-c", `user.name="${config.git.author.name}"`, "-c", `user.email="${config.git.author.email}"`]; } } module.exports = GitUtil; ================================================ FILE: back-filesystem/src/logger.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const logger = require("@oo/shared").logger; // Enable all log levels by default logger.debug.enable("*"); // Customize formatArgs // Based on https://github.com/visionmedia/debug/blob/master/node.js logger.debug.formatArgs = function formatArgs() { var args = arguments; var useColors = this.useColors; var name = this.namespace; if (useColors) { var c = this.color; args[0] = " \u001b[3" + c + ";1m" + name + " " + "\u001b[0m" + args[0] + "\u001b[3" + c + "m" + " +" + logger.debug.humanize(this.diff) + "\u001b[0m"; } else { args[0] = name + " " + args[0]; } return args; }; ================================================ FILE: back-filesystem/src/mime.types ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . # Additional MIME Types used by the Octave Online application # GNU Octave Types # If these haven't ben defined before, then they are now! text/x-octave m application/x-octave-data mat text/plain octaverc ================================================ FILE: back-filesystem/src/working-util.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const async = require("async"); const fs = require("fs"); const path = require("path"); const mime = require("mime"); const charsetDetector = require("charset-detector"); const Iconv = require("iconv").Iconv; const logger = require("@oo/shared").logger; const config = require("@oo/shared").config; const crypto = require("crypto"); // Load extra MIME types mime.load(path.join(__dirname, "mime.types")); const ACCEPTABLE_MIME_REGEX = /^(text\/.*)$/; const UNACCEPTABLE_FILENAME_REGEX = /^(\..*|octave-\w+)$/; class WorkingUtil { constructor(workDir, logMemo) { this._log = logger(`working-util:${logMemo}`); this._mlog = logger(`working-util:${logMemo}:minor`); this.cwd = workDir; } listAll(next) { async.waterfall([ (_next) => { this._recursiveReaddir(this.cwd, 0, _next); }, (fileInfos, _next) => { const dict = {}; fileInfos.forEach((fileInfo) => { if (!fileInfo) return; let filename = fileInfo.filename; delete fileInfo.filename; dict[filename] = fileInfo; }); _next(null, dict); } ], next); } hasOctaverc(next) { fs.access(path.join(this.cwd, ".octaverc"), (err) => { if (err) { this._mlog.trace(".octaverc does not exist"); return next(null, false); } else { this._mlog.trace(".octaverc exists"); return next(null, true); } }); } _recursiveReaddir(directory, depth, next) { // Don't recurse more than 3 levels deep if (depth > 3) { return next(null, []); } async.waterfall([ (_next) => { this._mlog.trace("Entering directory", directory); fs.readdir(directory, _next); }, (files, _next) => { async.map(files, (filename, __next) => { let pathname = path.join(directory, filename); let relname = path.relative(this.cwd, pathname); // TODO: Can the following be removed? if (pathname === path.join(this.cwd, ".git")) { this._mlog.debug("Skipping .git in _recursiveReaddir"); return; } async.waterfall([ (___next) => { // lstat to prevent following symlinks fs.lstat(pathname, ___next); }, (stats, ___next) => { this._mlog.trace("Got lstats", relname, stats.isDirectory(), stats.isFile()); if (stats.isDirectory()) { return this._recursiveReaddir(pathname, depth+1, ___next); } else if (stats.isFile()) { return this._getFileInfo(filename, pathname, relname, stats, ___next); } else { return ___next(null, []); } } ], __next); }, (err, results) => { if (err) return _next(err); this._mlog.trace("Leaving directory", directory); _next(null, Array.prototype.concat.apply([], results)); }); } ], next); } // Endpoint for standalone getFileInfo (used by SIOFU) getFileInfo(filename, next) { let pathname = this._safePath(filename); let relname = path.relative(this.cwd, pathname); async.waterfall([ (_next) => { fs.lstat(pathname, _next); }, (stats, _next) => { this._getFileInfo(filename, pathname, relname, stats, _next); }, (arr, _next) => { _next(null, arr[0]); } ], next); } _getFileInfo(filename, pathname, relname, stats, next) { this._mlog.trace("Getting file info for", relname); let _mime = mime.lookup(filename); if (ACCEPTABLE_MIME_REGEX.test(_mime)) { if (stats.size > config.session.textFileSizeLimit) { // This file is too big. Do not perform any further processing on this file. // FIXME: Show a nice message to the end user to let them know why their file isn't being loaded this._log.debug("Skipping text file that is too big:", stats.size, filename); next(null, [{ filename: relname, isText: false }]); } else { fs.readFile(pathname, (err, buf) => { if (err) return next(err); buf = this._convertCharset(buf); next(null, [{ filename: relname, isText: true, content: buf.toString("base64") }]); }); } } else if (!UNACCEPTABLE_FILENAME_REGEX.test(filename)) { next(null, [{ filename: relname, isText: false }]); } else { next(null, []); } } _convertCharset(buf) { var encoding; // Detect and attempt to convert charset if (buf.length > 0) { try { let charsetResults = charsetDetector(buf); this._log.debug("Top charset match:", charsetResults?.[0]); encoding = charsetResults?.[0]?.charsetName || "UTF-8"; if (encoding !== "UTF-8"){ this._log.trace("Converting charset:", encoding); buf = new Iconv(encoding, "UTF-8").convert(buf); } } catch(err) { this._log.warn("Could not convert encoding:", encoding); } } // Convert line endings // TODO: Is there a better way than converting to a string here? buf = new Buffer(buf.toString("utf8").replace(/\r\n/g, "\n")); return buf; } saveFile(filename, value, next) { // Create backup of file in memory in case there are any I/O errors async.waterfall([ (_next) => { let dirname = path.dirname(this._safePath(filename)); // Callback to a function to remove/normalize extra arguments fs.mkdir(dirname, { recursive: true }, (e) => _next(e)); }, (_next) => { fs.readFile( this._safePath(filename), (err, buf) => { if (!err) return _next(null, buf); if (/ENOENT/.test(err.message)) { this._log.info("Creating new file:", filename); return _next(null, new Buffer(0)); } return _next(err); } ); }, (buf, _next) => { fs.writeFile( this._safePath(filename), value, (err) => { _next(null, buf, err); }); }, (buf, err, _next) => { if (err) { fs.writeFile( this._safePath(filename), buf, () => { _next(err); }); } else { async.nextTick(() => { _next(null); }); } }, (_next) => { return _next(null, crypto.createHash("md5").update(value, "utf-8").digest("hex")); } ], next); } renameFile(oldname, newname, next) { fs.rename( this._safePath(oldname), this._safePath(newname), next); } deleteFile(filename, next) { fs.unlink( this._safePath(filename), next); } readBinary(filename, next) { async.waterfall([ (_next) => { fs.readFile(this._safePath(filename), _next); }, (buf, _next) => { const base64data = buf.toString("base64"); const _mime = mime.lookup(filename); _next(null, base64data, _mime); } ], next); } _safePath(filename) { // Filenames must descend from the working directory. Forbids filenames starting with '..' or similar. let candidate = path.join(this.cwd, filename); if (candidate.substring(0, this.cwd.length) !== this.cwd) { this._log.warn("Processed a fishy filename:", filename); return path.join(this.cwd, "octave-workspace"); // an arbitrary legal path } return candidate; } } module.exports = WorkingUtil; ================================================ FILE: back-master/.eslintrc.yml ================================================ ================================================ FILE: back-master/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: back-master/README.md ================================================ Octave Online Server: Back Server ================================= This directory contains the source code for the Octave Online Server back server. This is the "master" process that communicates with the downstream user via Redis and with the GNU Octave subprocess. Commands dealing with the filesystem are in the *back-filesystem* directory parallel to this directory. ## Setup There are three modes for the back server. 1. SELinux: Fast, and able to handle many concurrent sessions. 2. Unsafe: Fast and easy, but not recommended with untrusted users. Every Octave process is run without any sandboxing or resource limitations. Previously, a third mode, "docker", worked by creating a new Docker container for each session. This is no longer supported. Instead, if running with trusted users, you can use a single Docker container and Unsafe mode. ### Option 1: SELinux Ensure that you are running on CentOS or another distribution of Linux that supports SELinux. SELinux should come pre-installed on CentOS. Make and build Octave from source. Follow a procedure similar to the one put forth in *dockerfiles/build-octave.dockerfile*. Run `sudo yum install -y selinux-policy-devel policycoreutils-sandbox selinux-policy-sandbox libcgroup-tools` Ensure that Node.js is installed and the dependencies are downloaded for the shared project: $ (cd shared && npm install) Run all of the following make commands from the projects directory. - `sudo make install-cgroup` - `sudo make install-selinux-policy` - `sudo make install-selinux-bin` - `sudo make install-site-m` ### Option 2: Unsafe Follow the Option 2 instructions to build and install Octave from source. Stop before installing selinux-policy-devel and other selinux packages. ## Running the Back Server ### Debugging Go to the *back-master* directory and run `DEBUG=* node app.js` to start the back server. The `DEBUG=*` enables debug logging. ### Production `node app.js` can be run directly, but consider using `oo.service` in the *entrypoint* directory parallel to this directory. ## Stopping the Back Server By default, after the back server receives a successful maintenance request, it will wait for all sessions to close, and then cleanup and exit with code 0. If you wish to cause the back server process to gracefully release all sessions without exiting, you can send the signal SIGUSR1 to the process. ## To-do list - Update /usr/bin/sandbox according to https://github.com/SELinuxProject/selinux/commit/0f4620d6111838ce78bf5a591bb80c99c9d88730 - If using RHEL, the line "Defaults requiretty" must be commented out. ================================================ FILE: back-master/app.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const log = require("@oo/shared").logger("app"); const mlog = require("@oo/shared").logger("app:minor"); const hostname = require("@oo/shared").hostname(); const MessageTranslator = require("./src/message-translator"); const RedisMessenger = require("@oo/shared").RedisMessenger; const SessionManager = require("./src/session-manager"); const config = require("@oo/shared").config; const gcStats = (require("@sematext/gc-stats"))(); const fs = require("fs"); const path = require("path"); const async = require("async"); const http = require("http"); process.stdout.write("Process ID: " + process.pid + "\n"); process.stderr.write("Process ID: " + process.pid + "\n"); log.info("Process ID:", process.pid); log.info("Hostname:", hostname); log.log(process.env); var sessionManager, mainImpl, personality; if (fs.existsSync(config.rackspace.personality_filename)) { personality = JSON.parse(fs.readFileSync(config.rackspace.personality_filename, "utf8")); log.info("Personality:", personality.flavor, personality); sessionManager = new SessionManager(true); mainImpl = require("./src/main-flavor"); } else if (process.env["OO_FLAVOR_OVERRIDE"]) { personality = { flavor: process.env["OO_FLAVOR_OVERRIDE"] }; log.info("Flavor override:", personality.flavor); sessionManager = new SessionManager(true); mainImpl = require("./src/main-flavor"); } else { log.info("No personality file found"); personality = null; sessionManager = new SessionManager(false); mainImpl = require("./src/main-pool"); } let sessionLogDirCount = 0; function makeSessionLogDir(tokens) { if (tokens.length === config.worker.sessionLogs.depth) { const dirname = path.join(config.worker.logDir, config.worker.sessionLogs.subdir, ...tokens); if (sessionLogDirCount % 1000 === 0) { mlog.trace(dirname); } sessionLogDirCount++; fs.mkdirSync(dirname, { recursive: true }); } else { for (let a of "0123456789abcdef") { makeSessionLogDir(tokens.concat([a])); } } } log.info("Creating session log dirs…"); makeSessionLogDir([]); log.info(sessionLogDirCount, "dirs touched"); const redisInputHandler = new RedisMessenger().subscribeToInput(); const redisDestroyDHandler = new RedisMessenger().subscribeToDestroyD(); const redisExpireHandler = new RedisMessenger().subscribeToExpired(); const redisScriptHandler = new RedisMessenger().enableSessCodeScriptsSync(); const redisMessenger = new RedisMessenger(); const translator = new MessageTranslator(); redisInputHandler.on("message", translator.fromDownstream.bind(translator)); sessionManager.on("message", translator.fromUpstream.bind(translator)); translator.on("for-upstream", (sessCode, name, getData) => { const session = sessionManager.get(sessCode); // Stop processing this message if it doesn't have to do with a session running on this node. if (!session) return; // Now we can safely continue. The following method will download the data from Redis. session.enqueueMessage(name, getData); }); translator.on("for-downstream", (sessCode, name, content) => { log.trace("Sending Downstream:", sessCode, name); redisMessenger.output(sessCode, name, content); }); translator.on("destroy", (sessCode, reason) => { log.debug("Received Destroy:", sessCode); sessionManager.destroy(sessCode, reason); }); translator.on("ping", (code) => { // Not currently used log.debug("Received Ping:", code); redisMessenger.output(code, "pong", { hostname }); }); redisDestroyDHandler.on("destroy-d", (sessCode, reason) => { if (!sessionManager.get(sessCode)) return; log.info("Received Destroy-D:", sessCode, reason); sessionManager.destroy(sessCode, reason); }); redisExpireHandler.on("expired", (sessCode, channel) => { if (!sessionManager.get(sessCode)) return; log.info("Received Expire:", sessCode, channel); sessionManager.destroy(sessCode, "Octave Session Expired (downstream)"); }); sessionManager.on("touch", (sessCode, start) => { redisMessenger.touchOutput(sessCode); if (personality) { redisMessenger.output(sessCode, "oo.touch-flavor", { start, current: new Date().valueOf(), flavor: personality.flavor }); } }); sessionManager.on("live", (sessCode) => { redisMessenger.setLive(sessCode); }); sessionManager.on("destroy-u", (sessCode, reason) => { log.info("Sending Destroy-U:", reason, sessCode); redisMessenger.destroyU(sessCode, reason); }); gcStats.on("stats", (stats) => { mlog.trace(`Garbage Collected (type ${stats.gctype}, ${stats.pause/1e6} ms)`); }); const healthServer = http.createServer((req, res) => { if (sessionManager.isHealthy()) { res.writeHead(200); } else { res.writeHead(503); } res.end(); }).listen(config.gcp.health_check_port); mainImpl.start({ sessionManager, redisScriptHandler, redisMessenger, personality }, (err) => { log.error("Main-impl ended", err); doExit(); }); function doExit() { sessionManager.terminate("Server Maintenance"); mainImpl.doExit(); setTimeout(() => { redisInputHandler.close(); redisDestroyDHandler.close(); redisExpireHandler.close(); redisScriptHandler.close(); redisMessenger.close(); healthServer.close(); }, 5000); } function doGracefulExit() { log.info("RECEIVED SIGUSR1. Will not accept any further sessions."); mainImpl.doExit(); sessionManager.disablePool(); async.series([ (_next) => { async.whilst( () => { return sessionManager.numActiveSessions() > 0; }, (next) => { setTimeout(next, config.maintenance.pauseDuration); }, _next ); }, (_next) => { log.info("All user sessions are closed. Send SIGINT to fully terminate."); _next(null); } ], (err) => { if (err) log.error("Error during graceful exit:", err); }); } function doFastExit() { log.info("RECEIVED SIGNAL. Starting exit procedure."); doExit(); } process.on("SIGINT", doFastExit); process.on("SIGHUP", doFastExit); process.on("SIGTERM", doFastExit); process.on("SIGUSR1", doGracefulExit); //const heapdump = require("heapdump"); //setInterval(() => { heapdump.writeSnapshot("/srv/oo/logs/heap/" + hostname + "." + process.pid + "." + Date.now() + ".heapsnapshot"); }, 30000); ================================================ FILE: back-master/package.json ================================================ { "name": "@oo/back", "version": "0.0.0", "description": "Spawns Octave sessions and passes messages", "main": "app.js", "scripts": {}, "author": "Octave Online LLC", "license": "AGPL-3.0", "private": true, "engines": { "node": "18.x" }, "dependencies": { "@oo/shared": "file:../shared", "@sematext/gc-stats": "^1.5.8", "async": "^1.5.2", "base-x": "^3.0.8", "ps-tree": "^1.2.0", "temp": "^0.8.4", "tmp": "0.0.28", "uuid": "^2.0.1" } } ================================================ FILE: back-master/src/capped-file-system.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const async = require("async"); const temp = require("temp"); const child_process = require("child_process"); const fs = require("fs"); const path = require("path"); const logger = require("@oo/shared").logger; const OnlineOffline = require("@oo/shared").OnlineOffline; const config = require("@oo/shared").config; // This file is based on http://souptonuts.sourceforge.net/quota_tutorial.html const IMG_FILE_NAME = "img.ext3"; const IMG_MNT_DIR = "mnt"; const IMG_DATA_DIR = "data"; class CappedFileSystem extends OnlineOffline { constructor(sessCode, size) { super(); this.sessCode = sessCode; this._log = logger("capped-file-system:" + sessCode); this._mlog = logger("capped-file-system:" + sessCode + ":minor"); this._size = size; } _doCreate(next) { this._cleanups = []; async.series([ (_next) => { this._mlog.trace("Making temp dir..."); temp.mkdir("oo-", (err, tmpdir) => { if (tmpdir) this._tmpdir = tmpdir; if (!err) this._cleanups.unshift((__next) => { this._mlog.trace("Removing temp dir..."); fs.rmdir(tmpdir, __next); }); _next(err); }); }, (_next) => { this._mlog.debug("Created temp dir:", this._tmpdir); this._mlog.trace("Allocating space for filesystem..."); const imgFileName = path.join(this._tmpdir, IMG_FILE_NAME); // eslint-disable-next-line no-unused-vars child_process.execFile("dd", ["if=/dev/zero", `of=${imgFileName}`, "bs=1k", `count=${this._size}`], (err, stdout, stderr) => { if (!err) this._cleanups.unshift((__next) => { this._mlog.trace("Removing file system..."); fs.unlink(imgFileName, __next); }); _next(err); }); }, (_next) => { this._mlog.trace("Formatting file system..."); const imgFileName = path.join(this._tmpdir, IMG_FILE_NAME); child_process.execFile("mkfs", ["-t", "ext3", "-q", imgFileName, "-F"], (err, stdout, stderr) => { if (stderr) err = new Error(stderr); _next(err); }); }, (_next) => { this._mlog.trace("Creating mount directory..."); const imgMntDir = path.join(this._tmpdir, IMG_MNT_DIR); fs.mkdir(imgMntDir, 0o700, (err) => { if (!err) this._cleanups.unshift((__next) => { this._mlog.trace("Removing mount directory..."); fs.rmdir(imgMntDir, __next); }); _next(err); }); }, (_next) => { this._mlog.trace("Mounting file system..."); const imgFileName = path.join(this._tmpdir, IMG_FILE_NAME); const imgMntDir = path.join(this._tmpdir, IMG_MNT_DIR); child_process.execFile("sudo", ["mount", "-o", "loop,rw", imgFileName, imgMntDir], (err, stdout, stderr) => { if (stderr) err = new Error(stderr); if (!err) this._cleanups.unshift((__next) => { this._mlog.trace("Unmounting file system..."); child_process.execFile("sudo", ["umount", imgMntDir], __next); }); _next(err); }); }, (_next) => { this._mlog.trace("Claiming ownership of file system root..."); const imgMntDir = path.join(this._tmpdir, IMG_MNT_DIR); child_process.execFile("sudo", ["chown", config.worker.uid+":"+config.worker.uid, imgMntDir], (err, stdout, stderr) => { if (stderr) err = new Error(stderr); _next(err); }); }, (_next) => { this._mlog.trace("Creating data directory..."); const imgDataDir = path.join(this._tmpdir, IMG_MNT_DIR, IMG_DATA_DIR); fs.mkdir(imgDataDir, 0o700, (err) => { // Cleanup function not necessary here because the directory resides in the guest filesystem _next(err); }); } ], (err) => { const imgDataDir = path.join(this._tmpdir, IMG_MNT_DIR, IMG_DATA_DIR); this.dir = imgDataDir; return next(err, imgDataDir); }); } _doDestroy(next) { async.series(this._cleanups, (err) => { if (err) return next(err); this._enabled = false; this._cleanups = null; this._tmpdir = null; this.dir = null; return next(null); }); } } /// An alternative to CappedFileSystem when sudo is unavailable. /// This class does *not* limit file sizes. class TmpWorkDirectory extends OnlineOffline { constructor(sessCode) { super(); this.sessCode = sessCode; this._mlog = logger("tmp-work-directory:" + sessCode + ":minor"); } _doCreate(next) { this._cleanups = []; async.series([ (_next) => { this._mlog.trace("Making directory..."); temp.mkdir("oo-", (err, tmpdir) => { if (tmpdir) this.dir = tmpdir; if (!err) this._cleanups.unshift((__next) => { this._mlog.trace("Removing directory..."); child_process.execFile("rm", ["-rf", tmpdir], __next); }); this._mlog.debug("Created directory:", this.dir); _next(err); }); }, ], (err) => { return next(err, this.dir); }); } _doDestroy(next) { async.series(this._cleanups, (err) => { if (err) return next(err); this._enabled = false; this._cleanups = null; this.dir = null; return next(null); }); } } module.exports = { CappedFileSystem, TmpWorkDirectory }; ================================================ FILE: back-master/src/docker-handler.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const async = require("async"); const child_process = require("child_process"); const logger = require("@oo/shared").logger; const StdioMessenger = require("@oo/shared").StdioMessenger; class DockerHandler extends StdioMessenger { constructor(sessCode, dockerImage) { super(); this._log = logger(`docker-handler:${dockerImage}:${sessCode}`); this.sessCode = sessCode; } _doCreate(next, dockerArgs) { async.series([ (_next) => { // Create the session this._spwn = child_process.spawn("docker", dockerArgs); this._log.trace("Docker args:", dockerArgs.join(" ")); this._log.debug("Launched process with ID:", this._spwn.pid); // Create stderr listener this._spwn.stderr.on("data", this._handleLog.bind(this)); // Create exit listener this._spwn.on("exit", this._handleExit.bind(this)); // Listen to main read stream this.setReadStream(this._spwn.stdout); // Wait until we get an acknowledgement before continuing. Two conditions: receipt of the acknowledgement message, and premature exit. var ack = false; this.once("message", (name /*, content */) => { if (ack) return; ack = true; // Error if the message is docker-exit if (name === "docker-exit") return _next(new Error("Process exited prematurely")); // Don't enable the write stream until down here because we don't want to write messages to the child's STDIN until we've acknowledged that it is online this.setWriteStream(this._spwn.stdin); _next(null); }); } ], (err) => { if (err) return next(err); this._log.debug("Finished creating"); return next(null); }); } _doDestroy(next) { // Since the child process is actually the docker client and not the daemon, the SIGKILL will never get forwarded to the actual octave host process. We need to delegate the task to docker. child_process.execFile("docker", ["stop", "-t", 0, this._dockerName], (err, stdout, stderr) => { // child_process.execFile("docker", ["rm", "-f", this._dockerName], (err, stdout, stderr) => { if (err) this._log.warn(err, stderr); this._log.debug("Finished destroying"); return next(null); }); } signal(name) { if (this._state !== "ONLINE") return this._log.warn("Will not send SIGINT to child process: process not online"); // Although the child process is actually the docker client and not the daemon, the client will forward simple signals like SIGINT to the actual octave host process. this._spwn.kill(name); this._log.debug("Sent " + name + " to child process"); } _handleLog(data) { // Log message to console data.toString().trim().split("\n").forEach((line) => { this._log.log(line); }); // Special handling of certain messages // TODO: Make this message get sent from host.c instead of from here if (/Process exited with status 0, signal 9/.test(data)) { this.emit("message", "octave-killed"); } } _handleExit(code, signal) { this._log.debug("Docker Exit:", code, signal); this.emit("message", "docker-exit", { code, signal }); } } module.exports = DockerHandler; ================================================ FILE: back-master/src/main-flavor.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // This file contains the main loop for flavor (dedicated) servers. const log = require("@oo/shared").logger("main-flavor"); const mlog = require("@oo/shared").logger("main-flavor:minor"); const config = require("@oo/shared").config; const config2 = require("@oo/shared").config2; const RedisMessenger = require("@oo/shared").RedisMessenger; const MaintenanceRequestFlavorManager = require("./maintenance-request-manager"); const runMaintenance = require("./maintenance"); const async = require("async"); const rackOperations = require("@oo/shared/rack/operations"); var redisFlavorStatusHandler, flavorStatusManager; var ACCEPT_CONS = true; var statusTimer; function start(globals, next) { async.parallel([ (_next) => { startConnectionAcceptLoop(globals, _next); }, (_next) => { startLifetimeLoop(globals, _next); } ], next); } function startConnectionAcceptLoop(globals, next) { let { sessionManager, redisScriptHandler, personality } = globals; async.during( (_next) => { if (!ACCEPT_CONS) return _next(null, false); async.waterfall([ (__next) => { if (sessionManager.canAcceptNewSessions()) { redisScriptHandler.getSessCodeFlavor(personality.flavor, (err, sessCode, content) => { if (err) log.error("Error getting sessCode:", err); __next(null, sessCode, content); }); } else { process.nextTick(() => { __next(null, null, null); }); } }, (sessCode, content, __next) => { if (sessCode) { async.waterfall([ (___next) => { log.info("Received Session:", sessCode); content.tier = "_maxima"; sessionManager.attach(sessCode, content); sessionManager.disablePool(); flavorStatusManager.stop(); flavorStatusManager.ignoreAll(); clearTimeout(statusTimer); sessionManager.once("destroy-done", (_sessCode) => { ___next(null, _sessCode); }); }, (_sessCode, ___next) => { if (sessionManager.numActiveSessions() !== 0) { log.error("Active sessions when session was closed"); } if (sessCode !== _sessCode) { log.error("sessCode changed:", sessCode, _sessCode); } sessionManager.terminate("Server Maintenance"); setTimeout(___next, config.maintenance.pauseDuration); }, (___next) => { runMaintenance(___next); } ], (err) => { __next(err, false); }); } else { __next(null, true); } }, ], _next); }, (_next) => { let delay = Math.floor(config.worker.clockInterval.min + Math.random()*(config.worker.clockInterval.max-config.worker.clockInterval.min)); setTimeout(_next, delay); }, (err) => { mlog.info("Connection-accepting loop ended"); return next(err); } ); } function startLifetimeLoop(globals, next) { let { sessionManager, redisMessenger, personality } = globals; const flavorConfig = config2.flavor(personality.flavor); flavorStatusManager = new MaintenanceRequestFlavorManager(flavorConfig.defaultClusterSize); redisFlavorStatusHandler = new RedisMessenger().subscribeToFlavorStatus(personality.flavor); redisFlavorStatusHandler.on("flavor-status", (...args) => { flavorStatusManager.onMessage(...args); }); flavorStatusManager.on("request-maintenance", (...args) => { redisMessenger.requestFlavorStatus(personality.flavor, ...args); }); flavorStatusManager.on("reply-to-maintenance-request", (...args) => { redisMessenger.replyToFlavorStatus(personality.flavor, ...args); }); // In a flavor cluster, when we get approval from peers, we remove ourself from the cluster permanently in order to save on idle time costs. New servers will be added to the cluster when needed (on demand). async.forever( (_next) => { async.series([ (__next) => { statusTimer = setTimeout(__next, flavorConfig.idleTime); }, (__next) => { flavorStatusManager.beginRequesting(flavorConfig.statusInterval); flavorStatusManager.once("maintenance-accepted", __next); }, (__next) => { sessionManager.disablePool(); if (sessionManager.numActiveSessions() !== 0) { log.warn("Active sessions when flavor maintenance was approved"); return; } sessionManager.terminate("Server Maintenance"); statusTimer = setTimeout(__next, config.maintenance.pauseDuration); }, (__next) => { // TODO: Move this call somewhere it could be configurable. rackOperations.deleteSelf(personality, __next); } ], (err) => { _next(err); }); }, (err) => { log.info("Lifetime loop ended"); return next(err); } ); } function doExit() { clearTimeout(statusTimer); flavorStatusManager.stop(); ACCEPT_CONS = false; setTimeout(() => { redisFlavorStatusHandler.close(); }, 5000); } module.exports = { start, doExit }; ================================================ FILE: back-master/src/main-pool.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // This file contains the main loop for normal pool servers. const log = require("@oo/shared").logger("main-pool"); const config = require("@oo/shared").config; const RedisMessenger = require("@oo/shared").RedisMessenger; const MaintenanceRequestFlavorManager = require("./maintenance-request-manager"); const runMaintenance = require("./maintenance"); const async = require("async"); const redisMaintenanceHandler = new RedisMessenger().subscribeToRebootRequests(); const maintenanceRequestManager = new MaintenanceRequestFlavorManager(); var ACCEPT_CONS = true; var maintenanceTimer; // Main loops function start(globals, next) { async.parallel([ (_next) => { startConnectionAcceptLoop(globals, _next); }, (_next) => { startMaintenanceLoop(globals, _next); } ], next); } function startConnectionAcceptLoop(globals, next) { let { sessionManager, redisScriptHandler } = globals; async.forever( (_next) => { if (!ACCEPT_CONS) return; async.waterfall([ (__next) => { let fraction; if (config.worker.clockStrategy === "random") { fraction = Math.random(); } else { fraction = sessionManager.usagePercent(); } let delay = Math.floor(config.worker.clockInterval.min + fraction * (config.worker.clockInterval.max-config.worker.clockInterval.min)); setTimeout(__next, delay); }, (__next) => { if (sessionManager.canAcceptNewSessions()) { redisScriptHandler.getSessCode((err, sessCode, content) => { if (err) log.error("Error getting sessCode:", err); __next(null, sessCode, content); }); } else { process.nextTick(() => { __next(null, null, null); }); } }, (sessCode, content, __next) => { if (sessCode) { log.info("Received Session:", sessCode); sessionManager.attach(sessCode, content); } __next(null); } ], _next); }, (err) => { log.error("Connection-accepting loop ended"); return next(err); } ); } function startMaintenanceLoop(globals, next) { let { sessionManager, redisMessenger } = globals; redisMaintenanceHandler.on("reboot-request", maintenanceRequestManager.onMessage.bind(maintenanceRequestManager)); maintenanceRequestManager.on("request-maintenance", redisMessenger.requestReboot.bind(redisMessenger)); maintenanceRequestManager.on("reply-to-maintenance-request", redisMessenger.replyToRebootRequest.bind(redisMessenger)); async.forever( (_next) => { async.series([ (__next) => { maintenanceTimer = setTimeout(__next, config.maintenance.interval); }, (__next) => { maintenanceRequestManager.beginRequesting(config.maintenance.requestInterval); maintenanceRequestManager.once("maintenance-accepted", __next); }, (__next) => { sessionManager.disablePool(); async.whilst( () => { return sessionManager.numActiveSessions() > 0; }, (__next) => { maintenanceTimer = setTimeout(__next, config.maintenance.pauseDuration); }, __next ); }, (__next) => { sessionManager.terminate(); maintenanceTimer = setTimeout(__next, config.maintenance.pauseDuration); }, (__next) => { runMaintenance(__next); }, (__next) => { sessionManager.restart(); maintenanceTimer = setTimeout(__next, config.maintenance.pauseDuration); }, (__next) => { maintenanceRequestManager.reset(); __next(); } ], (err) => { _next(err); }); }, (err) => { log.error("Maintenance loop ended"); return next(err); } ); } function doExit() { clearTimeout(maintenanceTimer); maintenanceRequestManager.stop(); ACCEPT_CONS = false; setTimeout(() => { redisMaintenanceHandler.close(); }, 5000); } module.exports = { start, doExit }; ================================================ FILE: back-master/src/maintenance-request-manager.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const uuid = require("uuid"); const EventEmitter = require("events"); const log = require("@oo/shared").logger("maint-req-manager"); const config = require("@oo/shared").config; class MaintenanceRequestFlavorManager extends EventEmitter { constructor(clusterSize) { super(); if (clusterSize) { this._forFlavor = true; this._clusterSize = clusterSize; this._priority = Math.random(); log.debug("Flavor Priority:", this._priority); } this.reset(); } reset() { this._responses = {}; this._ignoreAll = false; if (!this._forFlavor) { this._priority = 0; } } ignoreAll() { this._ignoreAll = true; } beginRequesting(intervalTime) { log.info("Beginning maintenance requests"); this.reset(); this._requestInterval = setInterval(this._requestMaintenance.bind(this), intervalTime); } onMessage(id, isRequest, message) { // Has the server has removed itself from the actively responding cluster? if (this._ignoreAll) { return; } let isOwnRequest = Object.keys(this._responses).indexOf(id) !== -1; if (isRequest && !isOwnRequest) { // Reply to the maintenance request. Reply "yes" only if the requester's priority is higher than our own priority. let response = (message.priority > this._priority); this.emit("reply-to-maintenance-request", id, response); } else if (!isRequest && isOwnRequest) { // Someone replied to our own maintenance request. this._responses[id].push(message.response); } } stop() { if (this._requestInterval) { clearInterval(this._requestInterval); } this._requestInterval = null; } _requestMaintenance() { let id = uuid.v4(); if (!this._forFlavor) { // In flavor status mode, keep a fixed priority at all times. This creates stability and uniqueness that is easy to reason about. // In maintenance request mode, make the priority grow as time goes on. This means older servers get first priority to maintenance. this._priority += 1; } this._responses[id] = []; this.emit("request-maintenance", id, this._priority); log.trace("Sent maintenance request:", id, this._priority); setTimeout(() => { // Count the number of yeses and nos. let numYes = this._responses[id].reduce((s,v) => { return s + (v ? 1 : 0); }, 0); let numNo = this._responses[id].length - numYes; let success; if (this._forFlavor) { // Policy: Guarantee at least _clusterSize online nodes success = numYes >= this._clusterSize; } else { // Policy: Guarantee at least minNodesInCluster online nodes and no more than maxNodesInMaintenance maintenance nodes. success = numNo < config.maintenance.maxNodesInMaintenance && numYes >= config.maintenance.minNodesInCluster; } // Were we successful? if (success) { log.info("Maintenance request was approved"); this.emit("maintenance-accepted"); this._priority = Number.MAX_VALUE; clearInterval(this._requestInterval); } else { log.trace("Maintenance request failed; trying again:", id); } // Dereference responses array delete this._responses[id]; }, config.maintenance.responseWaitTime); } } module.exports = MaintenanceRequestFlavorManager; ================================================ FILE: back-master/src/maintenance.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const child_process = require("child_process"); const log = require("@oo/shared").logger("maintenance"); const config = require("@oo/shared").config; function runMaintenance(next) { log.info("Starting Maintenance Routine"); switch (config.session.implementation) { case "docker": { let MAINTENANCE_COMMAND = "docker ps -a --filter \"status=exited\" --filter \"ancestor=oo/" + config.docker.images.octaveSuffix + "\" | cut -c -12 | xargs -n 1 docker rm -f; docker ps -a --filter \"status=exited\" --filter \"ancestor=oo/" + config.docker.images.filesystemSuffix + "\" | cut -c -12 | xargs -n 1 docker rm -f"; log.trace("Running command:", MAINTENANCE_COMMAND); child_process.exec(MAINTENANCE_COMMAND, (err, stdout, stderr) => { log.info("Finished Maintenance Routine"); if (err) log.warn(err); log.trace(stdout); log.trace(stderr); next(); }); break; } case "selinux": case "unsafe": // Exit this process and let the daemon running it clean up and restart process.exit(0); break; default: { log.error("Please provide a maintenance command for your implementation"); break; } } } module.exports = runMaintenance; ================================================ FILE: back-master/src/message-translator.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); const log = require("@oo/shared").logger("message-translator"); const crypto = require("crypto"); const uuid = require("uuid"); const config = require("@oo/shared").config; // This class "translates" messages between the older format expected by the downstream front end and the newer format expected by the upstream back ends (Octave host and file manager). class MessageTranslator extends EventEmitter { // When the upstream back ends send us a message: fromUpstream(sessCode, name, content) { switch(name) { // MESSAGES NEEDING TRANSLATION: // "request-input" is the newer version of "prompt", which provided a line number as an integer. The line number needs to be extracted via regex for backwards compatibility. case "request-input": { let match = content.match(/^octave:(\d+)>\s+$/); let line_number = -1; if (match) { line_number = parseInt(match[1]); } else { this._forDownstream(sessCode, "data", { type: "stdout", data: content }); } this._forDownstream(sessCode, "prompt", { line_number, prompt: content }); break; } // "out" and "err" need to be translated to "data" events case "out": { this._forDownstream(sessCode, "data", { type: "stdout", data: content }); break; } case "err": { // Send the error text doenstream this._forDownstream(sessCode, "data", { type: "stderr", data: content }); break; } // We need only the "ws" part of the "set-workspace" message case "set-workspace": { this._forDownstream(sessCode, "workspace", { vars: content.ws }); break; } // The new "show-static-plot" needs to be broken into "plotd" (plot data) and "plote" (plot finished) case "show-static-plot": { let id = uuid.v4(); this._forDownstream(sessCode, "plotd", { id: id, content: content.content }); this._forDownstream(sessCode, "plote", { id: id, md5: crypto.createHash("md5").update(content.content).digest("hex"), command_number: content.command_number }); break; } // "clc" control command: case "clear-screen": { this._forDownstream(sessCode, "ctrl", { command: "clc" }); break; } // "doc" control command: case "show-doc": { var url = content ? `https://octave.sourceforge.io/list_functions.php?q=${encodeURIComponent(content)}` : "https://octave.sourceforge.io/octave/overview.html"; this._forDownstream(sessCode, "ctrl", { command: `url=${url}` }); break; } // When come other command was suppressed due to length: case "message-too-long": { if (content.name === "show-static-plot") { log.trace("Plot message too long:", content); this._forDownstream(sessCode, "data", { type: "stderr", data: `Warning: Suppressed a large plot (${content.length} bytes).\nMaximum allowable length is ${content.max_length} bytes.\nTip: Try running "clf". If that does not work,\ntry generating a rasterized plot (e.g., imagesc)\ninstead of a vector plot.\n` }); } else { log.warn("Unknown message too long:", content); } break; } // The "exit" event from octave_link: case "exit": { this._forDownstream(sessCode, "data", { type: "exit", code: content }); break; } // The "exit" event from the child process: case "docker-exit": case "process-exit": { this.emit("destroy", sessCode, "Shell Exited"); break; } // The event for when the Octave process is killed: case "octave-killed": { this._forDownstream(sessCode, "data", { type: "stderr", data: "Error: Octave process killed.\nYou may have been using too much memory.\nYour memory cap is: " + config.docker.memoryShares + "\n" }); break; } // Turn "destroy" into "destroy" on this instance case "destroy": { this.emit("destroy", sessCode, content); break; } // Filesystem events: if any of them fail, do not let the events bubble up, but show their error messages on stderr case "saved": case "renamed": case "deleted": { // FIXME: The rendering of error messages should occur on the client side, not here. if (content && !content.success) { this._forDownstream(sessCode, "data", { type: "stderr", data: content.message+"\n" }); break; } else { this._forDownstream(sessCode, name, content); break; } } // File list event (change name from "filelist" to "user") case "filelist": { this._forDownstream(sessCode, "user", content); break; } // MESSAGES THAT CAN BE IGNORED: case "ack": case "set-history": { break; } // REMAINING MESSAGES: default: { this._forDownstream(sessCode, name, content); break; } } } // When the downstream client sends us a message: fromDownstream(sessCode, name, getData) { switch(name) { // MESSAGES NEEDING TRANSLATION: // "data" needs to be translated to the "cmd" event (which is a synonym for "request-input-answer") case "data": this._forUpstream(sessCode, "cmd", getData); break; // Translate "signal" to "interrupt" case "signal": this._forUpstream(sessCode, "interrupt", getData); break; // Emit ping/pong as an event case "oo.ping": this.emit("ping", sessCode, getData); break; case "oo.pong": this.emit("pong", sessCode, getData); break; // MESSAGES THAT CAN BE IGNORED: case "init": case "ot.cursor": case "ot.change": case "oo.reconnect": break; // REMAINING MESSAGES: default: this._forUpstream(sessCode, name, getData); break; } } _forDownstream(sessCode, name, content) { this.emit("for-downstream", sessCode, name, content); } _forUpstream(sessCode, name, getData) { this.emit("for-upstream", sessCode, name, getData); } } module.exports = MessageTranslator; ================================================ FILE: back-master/src/octave-session.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; // The code in this file is shared among all implementations of the Octave session. See session-impl.js for examples of implementations. const logger = require("@oo/shared").logger; const OnlineOffline = require("@oo/shared").OnlineOffline; const config = require("@oo/shared").config; const config2 = require("@oo/shared").config2; const timeLimit = require("@oo/shared").timeLimit; const fs = require("fs"); const path = require("path"); const async = require("async"); const url = require("url"); const http = require("http"); const https = require("https"); const querystring = require("querystring"); const uuid = require("uuid"); const RedisQueue = require("@oo/shared").RedisQueue; const base58 = require("base-x")("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); const temp = require("temp"); const onceMessage = require("@oo/shared").onceMessage; const child_process = require("child_process"); class OctaveSession extends OnlineOffline { constructor(sessCode, options) { super(); this.sessCode = sessCode; this._options = options; this._log = logger("octave-session:" + sessCode); this._mlog = logger("octave-session:" + sessCode + ":minor"); this._log.debug("Tier:", this._options.tier); this._extraTime = 0; this._countdownExtraTime = config2.tier(this._options.tier)["session.countdownExtraTime"]; this._countdownRequestTime = config2.tier(this._options.tier)["session.countdownRequestTime"]; this._legalTime = config.session.legalTime.guest; this._payloadLimit = config.session.payloadLimit.guest; this._resetPayload(); this._plotPngStore = {}; this._plotSvgStore = {}; this._suppressedWarningsStore = {}; this._redisQueue = new RedisQueue(sessCode); this._redisQueue.on("message", this.sendMessage.bind(this)); this._throttleCounter = 0; this._throttleTime = process.hrtime(); this.on("error", this._handleError.bind(this)); } _doCreate(next) { let subdir = path.join(config.worker.logDir, config.worker.sessionLogs.subdir); for (let i=0; i { if (this._countdownTimer) clearTimeout(this._countdownTimer); if (this._timewarnTimer) clearTimeout(this._timewarnTimer); if (this._timeoutTimer) clearTimeout(this._timeoutTimer); if (this._autoCommitTimer) clearInterval(this._autoCommitTimer); if (this._payloadInterruptTimer) clearTimeout(this._payloadInterruptTimer); _next(null); }, (_next) => { this._doDestroyImpl(_next, reason); }, (_next) => { if (this._sessionLogStream) this._sessionLogStream.end(reason); _next(null); } ], next); } interrupt() { this._signal("SIGINT"); } enqueueMessage(name, getData) { this._redisQueue.enqueueMessage(name, getData); } // COUNTDOWN METHODS: For interrupting the Octave kernel after a fixed number of seconds to ensure a fair distribution of CPU time. // Use an interval to signal Octave once after the first timeout and then repeatedly after that, until the kernel sends us a "request-input" event to signal that it is done processing commands. _startCountdown() { if (this._countdownTimer) return; if (this._state !== "ONLINE") return; this._countdownTimer = setTimeout(this._onCountdownEnd.bind(this), this._legalTime); } _endCountdown() { if (this._countdownTimer) { clearTimeout(this._countdownTimer); this._countdownTimer = null; } } _onCountdownEnd() { if (new Date().valueOf() - this._extraTime < this._countdownRequestTime + config.session.countdownRequestTimeBuffer) { // Add 15 seconds and don't send an interrupt signal this._log.trace("Extending countdown with extra time"); this._countdownTimer = setTimeout(this._onCountdownEnd.bind(this), this._countdownExtraTime); } else { // Send an interrupt signal now and again in 5 seconds this._log.trace("Interrupting execution due to countdown"); this.interrupt(); this.emit("message", "err", "!!! OUT OF TIME !!!\n"); this._countdownTimer = setTimeout(this._onCountdownEnd.bind(this), 5000); } } _addTime() { // This method gets called when the user clicks the "Add 15 Seconds" button on the front end. this._extraTime = new Date().valueOf(); } // TIMEOUT METHODS: For killing the Octave kernel after a fixed number of seconds to clear server resources when the client is inactive. resetTimeout() { if (this._state !== "ONLINE") return; if (this._timewarnTimer) clearTimeout(this._timewarnTimer); if (this._timeoutTimer) clearTimeout(this._timeoutTimer); const timewarnTime = config2.tier(this._options.tier)["session.timewarnTime"]; const timeoutTime = config2.tier(this._options.tier)["session.timeoutTime"]; this._mlog.trace("Resetting timeout:", timewarnTime, timeoutTime); this._timewarnTimer = setTimeout(() => { this.emit("message", "err", config.session.timewarnMessage+"\n"); }, timewarnTime); this._timeoutTimer = setTimeout(() => { this._log.info("Session Timeout"); this.emit("message", "destroy", "Session Timeout"); }, timeoutTime); } // PAYLOAD METHODS: For interrupting the Octave kernel after a large amount of stdout/stderr data to prevent infinite loops from using too much bandwidth. _resetPayload() { this._payloadSize = 0; this._payloadInterrupted = false; } _appendToPayload(content) { this._payloadSize += content.length; if (this._payloadSize > this._payloadLimit && !this._payloadInterrupted) { this._payloadInterrupted = true; // End the countdown, and instead give the user a specified amount of time to allow the process to continue from where it left off. this._signal("SIGSTOP"); this._endCountdown(); let payloadDelay = config.session.payloadAcknowledgeDelay; this._payloadInterruptTimer = setTimeout(() => { this._signal("SIGCONT"); this._signal("SIGINT"); // Send the error message after a small delay in order to let the output buffers flush first setTimeout(() => { this.emit("message", "err", "!!! PAYLOAD TOO LARGE !!!\n"); // Octave sometimes gets confused with the interrupt signal, so send an empty command to reset things. this._sendMessageToHost("cmd", ""); }, config.session.payloadMessageDelay); }, payloadDelay); // Tell the user how much time they have. this.emit("message", "payload-paused", { delay: payloadDelay }); } } _acknowledgePayload() { if (!this._payloadInterrupted) return this._log.warn("Attempting to acknowledge payload, but process is not currently paused"); this._log.trace("User manually acknowledged payload"); this._continueIfPaused(); this._resetPayload(); } _continueIfPaused() { if (!this._payloadInterrupted) return; this._log.trace("Continuing execution"); clearTimeout(this._payloadInterruptTimer); this._signal("SIGCONT"); this._startCountdown(); } // AUTO-COMMIT METHODS: For auto-committing the user's files on a fixed interval. _startAutoCommitLoop() { this._autoCommitTimer = setInterval(() => { this._log.debug("Requesting auto-commit..."); this._commit("Scripted auto-commit", this._handleError.bind(this)); }, config.git.autoCommitInterval); } _commit(comment, next) { // Set a 60-second time limit let _next = timeLimit(config.git.commitTimeLimit, [new Error("Out of time")], next); // Call the callback when a "committed" message is received this._onceMessageFromFiles("committed", () => { _next(null); }); // Request the commit this._sendMessageToFiles("commit", { comment }); } // SESSION LOG: Log all commands, input, and output to a log file _appendToSessionLog(type, content) { if (!this._sessionLogStream) return this._log.warn("Cannot log before created", { type, content }); if (this._sessionLogStream.closed) return this._log.warn("Cannot log to a closed stream:", { type, content }); this._sessionLogStream.write(type + ": " + content.replace(/\n/g, "\n" + type + ": ") + "\n"); } // PLOTTED PNG IMAGE METHODS: Convert image links to base-64 data URIs // TODO: A better way to do this would be to modify GNUPlot to directly save PNG images as base-64 URIs. I did it this way because I wanted to avoid having to maintain a fork from another major project. _convertPlotImages(content) { // Search the plot SVG for local PNG files that we need to load let imageNames = []; let regex = /xlink:href='(\w+).png'/g; let match; while ((match = regex.exec(content.content))) { imageNames.push(match[1]); } if (imageNames.length === 0) return false; // Enqueue the images for loading let id = uuid.v4(); let svgObj = { content: content.content, command_number: (content.command_number || -1), waitCount: 0 }; imageNames.forEach((name) => { let filename = name + ".png"; if (filename in this._plotPngStore) { return this._log.error("Plot image is already in the queue:", filename); } this._plotPngStore[filename] = id; svgObj.waitCount++; // Actually send the read-image job upstream: this._sendMessageToFiles("read-delete-binary", { filename }); }); this._plotSvgStore[id] = svgObj; this._log.debug(`Loading ${svgObj.waitCount} images for plot`, id); return true; } _onDeletedBinary(content) { if (content.filename in this._plotPngStore) { this._resolvePng(content); return true; } else if (!content.success) { // TODO: Implement a better way to resolve load errors. this._log.warn("Failed loading a plot image; discarding all pending plots"); this._plotPngStore = {}; this._plotSvgStore = {}; } else { return false; } } _resolvePng(content) { let filename = content.filename; let base64data = content.base64data; let id = this._plotPngStore[filename]; delete this._plotPngStore[filename]; let svgObj = this._plotSvgStore[id]; this._log.trace(`Loaded image '${filename}' for plot`, id); // Perform the substitution svgObj.content = svgObj.content.replace(`xlink:href='${filename}'`, `xlink:href='data:image/png;base64,${base64data}'`); // Have we loaded all of the images we need to replace? svgObj.waitCount--; if (svgObj.waitCount === 0) { this._log.debug("Loaded all images for plot", id); this.emit("message", "show-static-plot", { content: svgObj.content, command_number: svgObj.command_number }); delete this._plotSvgStore[id]; } } // URL METHODS: Perform URL requests on behalf of the user. Uses a hard-coded whitelist in the config file for filtering out legal URLs. _handleRequestUrl(content) { try { let urlObj = url.parse(content.url); // Check if the hostname is legal if (urlObj.hostname === null) { this._sendMessageToHost("request-url-answer", [false, "You must specify a URL of the form http://example.com/"]); return; } let isLegal = false; for (let pattern of config.session.urlreadPatterns) { if (new RegExp(pattern).test(urlObj.hostname)) { isLegal = true; break; } } if (!isLegal) { this._sendMessageToHost("request-url-answer", [false, `The hostname ${urlObj.hostname} is not whitelisted\nfor access by Octave Online. If you think this hostname\nshould be whitelisted, please open a support ticket.`]); return; } // Convert from the query param list to a query string let paramObj = {}; for (let i=0; i { this._log.trace("Received URL response:", res.statusCode, urlObj.href); res.setEncoding("base64"); let fullResult = ""; let errmsg = ""; res.on("data", (chunk) => { if (chunk.length + fullResult.length > config.session.urlreadMaxBytes) { errmsg = `Requested URL exceeds maximum of ${config.session.urlreadMaxBytes} bytes`; } else { fullResult += chunk; } }); res.on("end", () => { this._log.trace("URL response after processing:", errmsg, fullResult.length); if (errmsg) { this._sendMessageToHost("request-url-answer", [false, errmsg]); } else { this._sendMessageToHost("request-url-answer", [true, fullResult]); } }); }); req.on("error", (err) => { this._log.trace("Problem with URL request:", err.message); this._sendMessageToHost("request-url-answer", [false, err.message]); }); req.write(payload); req.end(); } catch(err) { this._sendMessageToHost("request-url-answer", [false, err.message]); } } // BUCKET METHODS: Create (and destroy) buckets with snapshots of static files that can be published. _createBucket(bucketInfo) { let filenames = bucketInfo.filenames; if (!Array.isArray(filenames)) return; // Create the bucket ID. // Note: Starting 2021-08-07, the bucket ID is generated on the front server instead. The if statement here is for backwards compatibility. let bucketId = bucketInfo.bucket_id; if (!bucketId) { const bucketIdBuffer = new Buffer(16); uuid.v4({}, bucketIdBuffer, 0); bucketId = base58.encode(bucketIdBuffer); bucketInfo.bucket_id = bucketId; } this._log.debug("Creating new bucket:", bucketId, filenames); async.auto({ "read_files": (_next) => { // Load the bucket files into memory. this._mlog.trace("Reading files for bucket"); let jobId = uuid.v4(); this._onceMessageFromFiles("multi-binary:" + jobId, (err, data) => { if (!data.success) return _next(new Error("Unsuccessful call to multi-binary")); _next(null, data.results); }, _next); this._sendMessageToFiles("multi-binary", { id: jobId, filenames: filenames }); }, "tmpdir": (_next) => { // We need to create a working directory for the bucket git this._mlog.trace("Creating tmpdir for bucket"); temp.mkdir("oo-", _next); }, "session": (_next) => { // Create a Git session for the new bucket. const session = this._makeNewFileSession("create-bucket:" + bucketId); session.on("message", (name /*, content */) => { this._mlog.trace("Bucket file session message:", name); }); session.on("error", (err) => { this._log.error("Bucket file session error:", err); }); _next(null, session); }, // NOTE: In Async 1.5.x, the version used here, the argument order is (_next, results), but in Async 2.x, the argument order changed to (results, _next). "session_create": ["tmpdir", "session", (_next, results) => { this._mlog.trace("Creating session for bucket"); results.session.create(_next, results.tmpdir); }], "session_init": ["session_create", (_next, results) => { this._mlog.trace("Initializing session for bucket"); onceMessage(results.session, "filelist", _next); results.session.sendMessage("bucket-info", { id: bucketId, readonly: false }); }], "write_files": ["read_files", "session_init", (_next, results) => { this._mlog.trace("Writing files for bucket"); let jobId = uuid.v4(); onceMessage(results.session, "multi-binary-saved:" + jobId, (err, data) => { if (!data.success) return _next(new Error("Unsuccessful call to save-multi-binary")); _next(null); }); results.session.sendMessage("save-multi-binary", { id: jobId, filenames: filenames, base64datas: results.read_files }); }], "commit": ["write_files", (_next, results) => { this._mlog.trace("Committing files for bucket"); onceMessage(results.session, "committed", _next); results.session.sendMessage("commit", { comment: "Scripted bucket creation: " + bucketId }); }], "destroy_session": ["commit", (_next, results) => { this._mlog.trace("Destroying session for bucket"); results.session.destroy(_next); }], "destroy_tmpdir": ["destroy_session", (_next, results) => { this._mlog.trace("Destroying working dir bucket"); child_process.exec(`rm -rf ${results.tmpdir}`, _next); }] }, (err) => { if (err) { this._log.error("Error creating bucket:", err); this.emit("message", "err", "Encountered an error creating the bucket.\n"); } else { this._log.info("Finished creating new bucket:", bucketId); this.emit("message", "bucket-repo-created", bucketInfo); } }); } // Prevent spammy messages from clogging up the server. // TODO: It would be better if this were done deeper in the stack, such as host.c, so that message spamming doesn't reach all the way into the main event loop. _checkThrottle() { // FIXME: Make these config values. // Nominal values: 100 messages per 100 milliseconds (1000 messages/second) = spam. // INTERVAL_DURATION should be less than 1e9. let MSGS_PER_INTERVAL = 100; let INTERVAL_DURATION = 1e8; if (++this._throttleCounter < MSGS_PER_INTERVAL) return; this._throttleCounter = 0; let diff = process.hrtime(this._throttleTime); this._throttleTime = process.hrtime(); if (diff[0] === 0 && diff[1] < INTERVAL_DURATION) { this._log.warn("Messages too rapid! Killing process!", diff); this.emit("message", "destroy", "Too Many Packets"); } } sendMessage(name, content) { switch (name) { // Messages requiring special handling case "interrupt": this._continueIfPaused(); this.interrupt(); break; case "oo.add_time": this._addTime(); this.resetTimeout(); break; case "oo.acknowledge_payload": this._acknowledgePayload(); this.resetTimeout(); break; case "cmd": // FIXME: The following translation (from content to content.data) should be performed in message-translator.js, but we're unable to do so because the data isn't downloaded from Redis until after message-translator is run. Is there a more elegant place to put this? Maybe all message translation should happen here in octave-session.js instead? content = content.data || ""; if (typeof content !== "string") { this._log.error("content is not a string:", content); break; } this._startCountdown(); this.resetTimeout(); this._appendToSessionLog(name, content); // Split the command into individual lines and send them to Octave one-by-one. content.split("\n").forEach((line) => { this._sendMessageToHost(name, line); }); break; case "user-info": if (content && content.user) { this._startAutoCommitLoop(); this._legalTime = content.user.legalTime; this._payloadLimit = content.user.payloadLimit; this._countdownExtraTime = content.user.countdownExtraTime; this._countdownRequestTime = content.user.countdownRequestTime; } if (content.bucketId && !content.bucket) { // For backwards compatibility content.bucket = { bucket_id: content.bucketId, butype: "readonly", }; } if (content.bucket) { if (!content.bucket.butype) { this._log.error("butype not present in bucket", content); content.butype = "readonly"; } this._sendMessageToFiles("bucket-info", { id: content.bucket.bucket_id, legalTime: this._legalTime, // For backwards compatibility readonly: content.bucket.butype === "readonly", }); } else { this._sendMessageToFiles(name, content); } break; case "oo.create_bucket": this._createBucket(content); break; // Messages to forward to the file manager case "list": case "refresh": case "commit": case "save": case "rename": case "delete": case "binary": case "read-delete-binary": case "siofu_start": case "siofu_progress": case "siofu_done": this._sendMessageToFiles(name, content); break; // Messages to forward to the Octave host case "request-input-answer": case "request-url-answer": case "confirm-shutdown-answer": case "prompt-new-edit-file-answer": case "message-dialog-answer": case "question-dialog-answer": case "list-dialog-answer": case "input-dialog-answer": case "file-dialog-answer": case "debug-cd-or-addpath-error-answer": this._sendMessageToHost(name, content); break; // Unknown messages default: this._log.debug("Message ignored:", name); break; } } _handleMessage(name, content) { // Check for message throttling, except for "out" and "err" messages, which are throttled via payload. if (name !== "out" && name !== "err") this._checkThrottle(); // Special pre-processing of a few events here switch (name) { case "show-static-plot": // Convert PNG file links to embedded base 64 data if (this._convertPlotImages(content)) return; break; case "deleted-binary": // If we're waiting for any binary files, capture them here rather than sending them downstream if (this._onDeletedBinary(content)) return; break; case "display-warning": let opWarningMatch = /(the '.*' operator)/.exec(content.message); if (opWarningMatch) { if (this._suppressedWarningsStore[opWarningMatch[0]]) { this._mlog.trace("Suppressed duplicate warning:", content.id); return; } this._suppressedWarningsStore[opWarningMatch[0]] = true; } name = "err"; content = content.formatted; break; case "display-exception": name = "err"; content = content.ee_str; break; default: break; } if (name === "err") { // Filter out some error messages if (/warning: readline is not linked/.test(content)) return; if (/warning: docstring file/.test(content)) return; if (/error: unable to open .+macros\.texi/.test(content)) return; if (/^\/tmp\/octave-help-/.test(content)) return; if (/built-in-docstrings' not found/.test(content)) return; } if (/^multi-binary:[\w-]+$/.test(name)) return; // Forward remaining events downstream this.emit("message", name, content); this.emit(`msg:${name}`, content); // Special post-processing of a few more events here switch (name) { case "request-input": this._endCountdown(); this.resetTimeout(); this._resetPayload(); break; case "err": case "out": this._appendToPayload(content); this._appendToSessionLog(name, content); break; case "request-url": this._handleRequestUrl(content); break; // UNIMPLEMENTED FEATURES REQUIRING RESPONSE: case "confirm-shutdown": this._sendMessageToHost("confirm-shutdown-answer", true); break; case "prompt-new-edit-file": this._sendMessageToHost("prompt-new-edit-file-answer", true); break; case "message-dialog": this._sendMessageToHost("message-dialog-answer", 0); break; case "question-dialog": this._sendMessageToHost("question-dialog-answer", ""); break; case "list-dialog": this._sendMessageToHost("list-dialog-answer", [[],0]); break; case "input-dialog": this._sendMessageToHost("input-dialog-answer", []); break; case "file-dialog": this._sendMessageToHost("file-dialog-answer", []); break; case "debug-cd-or-addpath-error": this._sendMessageToHost("debug-cd-or-addpath-error-answer", 0); break; default: break; } } _handleError(err) { if (err) this._log.error(err); } } module.exports = OctaveSession; ================================================ FILE: back-master/src/process-handler.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const async = require("async"); const logger = require("@oo/shared").logger; const StdioMessenger = require("@oo/shared").StdioMessenger; class ProcessHandler extends StdioMessenger { constructor(sessCode) { super(); this._log = logger(`process-handler:${sessCode}`); this._mlog = logger(`process-handler:${sessCode}:minor`); this.sessCode = sessCode; } _doCreate(next, fn) { async.series([ (_next) => { // Spawn the process let args = Array.prototype.slice.call(arguments, 2); this._mlog.trace("Spawning process:", args[0], args[1].join(" "), args[2]); this._spwn = fn.apply(this, args); // Create all unexpected error listeners this._spwn.on("error", (err) => { this._log.error("spwn:", err); }); this._spwn.stdin.on("error", (err) => { this._log.error("stdin:", err); }); this._spwn.stdout.on("error", (err) => { this._log.error("stdout:", err); }); this._spwn.stderr.on("error", (err) => { this._log.error("stderr:", err); }); // Create stderr listener this._spwn.stderr.on("data", this._handleLog.bind(this)); // Create exit listener this._spwn.on("exit", this._handleExit.bind(this)); // Listen to main read stream this.setReadStream(this._spwn.stdout); // Wait until we get an acknowledgement before continuing. Two conditions: receipt of the acknowledgement message, and premature exit. var ack = false; this.once("message", (name /*, content */) => { if (ack) return; ack = true; // Error if the message is process-exit if (name === "process-exit") return _next(new Error("Process exited prematurely")); // Don't enable the write stream until down here because we don't want to write messages to the child's STDIN until we've acknowledged that it is online this.setWriteStream(this._spwn.stdin); _next(null); }); } ], (err) => { if (err) return next(err); this._mlog.debug("Finished creating"); return next(null); }); } _doDestroy(next) { // This method wont't be called unless the process state is ONLINE, so we don't need to check. if (this._spwn) { // We can ignore the "next" callback because it will be implicitly called by _handleExit() this._mlog.trace("this._spwn exists"); this._doDestroyProcess(); } else { this._mlog.trace("this._spwn does not exist"); next(null); } } signal(name) { if (!this._spwn) return this._log.warn("Tried to signal child process, but it does not exist"); if (this._spwn.exitCode !== null || this._spwn.signalCode !== null) return this._log.warn("Tried to signal child process, but it is exited"); this._signal(name); this._log.debug("Sent " + name + " to child process"); } _signal(name) { this._spwn.kill(name); } _handleLog(data) { // Log message to console data.toString().trim().split("\n").forEach((line) => { this._mlog.log(line); }); } _handleExit(code, signal) { this._log.debug("Process Exit:", code, signal); this._spwn = null; this.emit("message", "process-exit", { code, signal }); this._internalDestroyed(null); // TODO: when to emit this.emit("message", "octave-killed") ? } } module.exports = ProcessHandler; ================================================ FILE: back-master/src/session-impl.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const OctaveSession = require("./octave-session"); const { CappedFileSystem, TmpWorkDirectory } = require("./capped-file-system"); const DockerHandler = require("./docker-handler"); const ProcessHandler = require("./process-handler"); const config = require("@oo/shared").config; const config2 = require("@oo/shared").config2; const async = require("async"); const silent = require("@oo/shared").silent; const child_process = require("child_process"); const logger = require("@oo/shared").logger; const pstree = require("ps-tree"); const temp = require("temp"); const OnlineOffline = require("@oo/shared").OnlineOffline; const Queue = require("@oo/shared").Queue; const onceMessage = require("@oo/shared").onceMessage; const FilesController = require("../../back-filesystem/src/controller"); class SessionImpl extends OctaveSession { constructor(sessCode, options) { super(sessCode, options); this.options = options; this._makeSessions(); if (config.session.implementation === "unsafe") { this._cfs = new TmpWorkDirectory(this.sessCode); } else { this._cfs = new CappedFileSystem(this.sessCode, config.docker.diskQuotaKiB); } this._filesSession.on("message", this._handleMessage.bind(this)); this._hostSession.on("message", this._handleMessage.bind(this)); this._cfs.on("error", this._handleError.bind(this)); this._filesSession.on("error", this._handleError.bind(this)); this._hostSession.on("error", this._handleError.bind(this)); } _doCreateImpl(next) { async.auto({ "cfs": (_next) => { this._mlog.trace("Requesting creation of capped file system"); this._cfs.create((err, dataDir) => { if (!err) this._dataDir = dataDir; _next(err); }); }, "files": ["cfs", (_next) => { this._mlog.trace("Requesting creation of file manager process"); this._filesSession.create(_next, this._dataDir); }], "host": ["cfs", (_next) => { this._mlog.trace("Requesting creation of Octave host process"); this._hostSession.create(_next, this._dataDir); }] }, (err) => { if (err) return next(err); this._log.info("Session successfully created"); this.resetTimeout(); return next(null); }); } _doDestroyImpl(next, reason) { // TODO: Add an alternative destroy implementation that is synchronous, so that it can be run in an exit handler. async.auto({ "commit": (_next) => { this._mlog.trace("Requesting to commit changes to Git"); this._commit("Scripted user file commit", silent(/Out of time/, _next)); }, "host": ["commit", (_next) => { this._mlog.trace("Requesting termination of Octave host process"); this._hostSession.destroy(_next); }], "files": ["commit", (_next) => { this._mlog.trace("Requesting termination of file manager process"); this._filesSession.destroy(_next); }], "cfs": ["host", "files", (_next) => { this._mlog.trace("Requesting deletion of capped file system"); this._cfs.destroy(_next); }] }, (err) => { if (err) return next(err); this._log.info("Session successfully destroyed:", reason); return next(null); }); } _signal(name) { this._hostSession.signal(name); } _sendMessageToFiles(name, content) { this._filesSession.sendMessage(name, content); } _sendMessageToHost(name, content) { this._mlog.trace("Sending message to Octave host:", name, content); this._hostSession.sendMessage(name, content); } _onceMessageFromFiles(name, next) { onceMessage(this._filesSession, name, next); } } class HostProcessHandler extends ProcessHandler { constructor(sessCode, options) { super(sessCode); this.options = options; // Override default logger with something that says "host" this._log = logger(`host-handler:${sessCode}`); this._mlog = logger(`host-handler:${sessCode}:minor`); } _doCreate(next, dataDir) { const tier = this.options.tier; let cgroupName = config2.tier(tier)["selinux.cgroup.name"]; let addressSpace = config2.tier(tier)["selinux.prlimit.addressSpace"]; const envVars = [ "env", "GNUTERM=svg", "env", "LD_LIBRARY_PATH=/usr/local/lib", "env", "OO_SESSCODE="+this.sessCode, "env", "OO_TIER="+this.options.tier ]; async.series([ (_next) => { temp.mkdir("oo-", (err, tmpdir) => { this._mlog.debug("Created tmpdir:", tmpdir); this.tmpdir = tmpdir; _next(err); }); }, (_next) => { if (config.session.implementation === "unsafe") { // Spawn un-sandboxed process super._doCreate(_next, child_process.spawn, "env", [].concat(envVars.slice(1)).concat(["/usr/local/bin/octave-host", config.session.jsonMaxMessageLength]), { cwd: dataDir }); } else { // Spawn sandboxed process // The CWD is set to /tmp in order to make the child process not hold a reference to the mount that the application happens to be running under. super._doCreate(_next, child_process.spawn, "/usr/bin/prlimit", [ "--as="+addressSpace, "/usr/bin/cgexec", "-g", "cpu:"+cgroupName, "/usr/bin/sandbox", "-M", "-H", dataDir, "-T", this.tmpdir, "--level", "s0"] .concat(envVars) .concat([ "/usr/local/bin/octave-host", config.session.jsonMaxMessageLength ]), { cwd: "/tmp" }); } }, (_next) => { // We need to get the octave-cli PID for signalling, because sandbox handles signals strangely. this.octavePID = null; async.whilst( () => { return !this.octavePID && this._state !== "DESTROYED"; }, (__next) => { async.waterfall([ (___next) => { setTimeout(___next, 250); }, (___next) => { this._mlog.trace("Attempting to get Octave PID..."); pstree(this._spwn.pid, ___next); }, (children, ___next) => { let child = children.find((_child) => { return /octave-cli/.test(_child.COMMAND); }); if (child) { this.octavePID = child.PID; this._mlog.debug("Got Octave PID:", this.octavePID); } ___next(null); } ], __next); }, _next ); } ], next); } _doDestroy(next) { async.series([ super._doDestroy.bind(this), (_next) => { if (this.tmpdir) { this._mlog.trace("Destroying tmpdir"); child_process.exec(`rm -rf ${this.tmpdir}`, _next); } else { process.nextTick(_next); } } ], next); } _doDestroyProcess() { // Starting with Octave 4.4, sending SIGTERM is insufficient to make Octave exit. this._log.trace("Executing 'exit' in Octave process"); this.sendMessage("cmd", "exit"); setTimeout(() => { if (!this._spwn) { this._mlog.trace("Not sending SIGKILL: Process is already exited"); return; } this._log.trace("Sending SIGKILL"); this._signal("SIGKILL"); }, 10000); } _signal(name) { if (!this.octavePID) return this._log.error("Cannot signal Octave process yet"); child_process.exec(`kill -s ${name.slice(3)} ${this.octavePID}`, (err) => { if (err) this._log.error("signalling octave:", err); }); } } class FilesControllerHandler extends OnlineOffline { constructor(sessCode) { super(); this._log = logger(`files-handler:${sessCode}`); this._mlog = logger(`files-handler:${sessCode}:minor`); this.sessCode = sessCode; this._messageQueue = new Queue(); } _doCreate(next, dataDir) { async.series([ (_next) => { // Make the gitdir temp.mkdir("oo-", (err, tmpdir) => { this._mlog.debug("Created gitdir:", tmpdir); this.gitdir = tmpdir; _next(err); }); }, (_next) => { // Make the controller this.controller = new FilesController(this.gitdir, dataDir, this.sessCode); // Flush messages to the controller while (!this._messageQueue.isEmpty()) this._flush(); this._messageQueue.on("enqueue", this._flush.bind(this)); // Emit messages from the controller this.controller.on("message", (name, content) => { this.emit("message", name, content); }); _next(null); } ], next); } _doDestroy(next) { async.series([ (_next) => { if (this.controller) { this.controller.destroy(); } if (this.gitdir) { this._mlog.trace("Destroying gitdir"); child_process.exec(`rm -rf ${this.gitdir}`, _next); } else { process.nextTick(_next); } } ], next); } sendMessage(name, content) { this._messageQueue.enqueue([name, content]); } _flush() { this.controller.receiveMessage.apply(this.controller, this._messageQueue.dequeue()); } } class SessionSELinux extends SessionImpl { _makeSessions() { this._filesSession = new FilesControllerHandler(this.sessCode); this._hostSession = new HostProcessHandler(this.sessCode, this.options); } _makeNewFileSession(sessCode) { return new FilesControllerHandler(sessCode); } } class HostDockerHandler extends DockerHandler { constructor(sessCode) { super(sessCode); this._dockerImage = config.docker.images.octaveSuffix; this._dockerName = `oo-host-${sessCode}`; // Override default logger with something that says "host" this._log = logger(`host-handler:${sessCode}`); this._mlog = logger(`host-handler:${sessCode}:minor`); } _doCreate(next, dataDir) { // More about resource management: https://goldmann.pl/blog/2014/09/11/resource-management-in-docker/ const dockerArgs = [ "run", "-i", "-v", `${dataDir}:${config.docker.cwd}`, "--cpu-shares", config.docker.cpuShares, "-m", config.docker.memoryShares, "--name", this._dockerName, `oo/${this._dockerImage}` ]; super._doCreate(next, dockerArgs); } } class FilesDockerHandler extends DockerHandler { constructor(sessCode) { super(sessCode); this._dockerImage = config.docker.images.filesystemSuffix; this._dockerName = `oo-files-${sessCode}`; // Override default logger with something that says "files" this._log = logger(`files-handler:${sessCode}`); this._mlog = logger(`files-handler:${sessCode}:minor`); } _doCreate(next, dataDir) { const dockerArgs = [ "run", "-i", "-v", `${dataDir}:${config.docker.cwd}`, "--name", this._dockerName, `oo/${this._dockerImage}` ]; super._doCreate(next, dockerArgs); } } class SessionDocker extends SessionImpl { _makeSessions() { this._filesSession = new FilesDockerHandler(this.sessCode); this._hostSession = new HostDockerHandler(this.sessCode); } _makeNewFileSession(sessCode) { return new FilesDockerHandler(sessCode); } } module.exports = { docker: SessionDocker, selinux: SessionSELinux, docker_handler: HostDockerHandler, selinux_handler: HostProcessHandler }; ================================================ FILE: back-master/src/session-manager.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const log = require("@oo/shared").logger("session-manager"); const mlog = require("@oo/shared").logger("session-manager:minor"); const EventEmitter = require("events"); const impls = require("./session-impl"); const uuid = require("uuid"); const Queue = require("@oo/shared").Queue; const config = require("@oo/shared").config; const config2 = require("@oo/shared").config2; const metrics = require("@oo/shared").metrics; const timeLimit = require("@oo/shared").timeLimit; class SessionManager extends EventEmitter { constructor(maxOnly) { super(); this._pool = {}; this._poolSizes = {}; let tiersEnabled; if (maxOnly) { tiersEnabled = ["_maxima"]; } else { tiersEnabled = Object.keys(config.tiers); tiersEnabled.splice(tiersEnabled.indexOf("_maxima"), 1); } tiersEnabled.forEach((tier) => { if (config2.tier(tier)["sessionManager.poolTier"]) { mlog.info("Skipping tier with poolTier:", tier); // continue } else { mlog.info("Enabling tier:", tier); this._pool[tier] = {}; this._poolSizes[tier] = config2.tier(tier)["sessionManager.poolSize"]; } }); log.info("Enabled tiers:", Object.keys(this._pool)); this._online = {}; this._monitor_session = null; this._setup(); this.startPool(); this.recordMetrics(); } _setup() { // Log the session index on a fixed interval this._logInterval = setInterval(() => { Object.keys(this._pool).forEach((tier) => { log.debug("Current Number of Pooled Sessions, tier " + tier + ":", Object.keys(this._pool[tier]).length); log.trace(Object.keys(this._pool[tier]).join("; ")); }); log.debug("Current Number of Online Sessions:", Object.keys(this._online).length); log.trace(Object.keys(this._online).join("; ")); // Time an arbitrary command to test server health if (this._monitor_session) { let t1 = new Date().valueOf(); this._monitor_session.sendMessage("cmd", { data: "pinv(magic(500));" }); this._monitor_session.once("msg:request-input", () => { const elapsed = new Date().valueOf() - t1; log.debug("Monitor Time (ms):", elapsed); metrics.gauge("oo.monitor_time_ms", elapsed); }); } }, config.sessionManager.logInterval); // Keep pool sessions alive this._keepAliveInterval = setInterval(() => { Object.keys(this._pool).forEach((tier) => { Object.keys(this._pool[tier]).forEach((localCode) => { this._pool[tier][localCode].session.resetTimeout(); }); }); }, config.session.timewarnTime/2); } numActiveSessions() { return Object.keys(this._online).length; } canAcceptNewSessions() { if (!this._poolVar.enabled) { return false; } if (this.numActiveSessions() >= config.worker.maxSessions) { return false; } for (let tier of Object.keys(this._pool)) { // Require every tier to have at least 1 session in the pool if (Object.keys(this._pool[tier]).length === 0) { return false; } } return true; } usagePercent() { return this.numActiveSessions() / config.worker.maxSessions; } isHealthy() { return this._monitor_session && this._monitor_session.isOnline(); } _create(next, options) { // Get the correct implementation const SessionImpl = config.session.implementation === "docker" ? impls.docker : impls.selinux; // Create the session object const localCode = uuid.v4(null, new Buffer(16)).toString("hex"); const session = new SessionImpl(localCode, options); // Add messages to a cache when they are created const cache = new Queue(); session.on("message", (name, content) => { cache.enqueue([name, content]); }); session.create(timeLimit(config.sessionManager.startupTimeLimit, [new Error("Time limit reached")], (err) => { // Get rid of the session if it failed to create if (err) { log.warn("Session failed to create:", localCode, err); session.destroy(null, "Failed To Create"); next(); return; } // Call the callback next(localCode, session, cache, options); })); } attach(remoteCode, content) { // Move pool session to online session if (!this.canAcceptNewSessions()) return log.warn("Cannot accept any new sessions right now"); // TODO: Backwards compatibility with old front server: the message content can be the user itself; if null, it is a guest user. if (!content) { content = { user: null }; } else if (content.parametrized) { content = { user: content }; } // Determine which tier to use // Important: the user in content.user is not necesarilly the currently authenticated user; it could be the owner of a shared workspace. const user = content.user; const tier = content.tier ? content.tier : user ? user.tier : Object.keys(this._pool)[0]; const poolTier = config2.tier(tier)["sessionManager.poolTier"] || tier; // eslint-disable-next-line no-console console.assert(Object.keys(this._pool).includes(poolTier), poolTier); // Pull from the pool const localCode = Object.keys(this._pool[poolTier])[0]; this._online[remoteCode] = this._pool[poolTier][localCode]; delete this._pool[poolTier][localCode]; log.info("Upgraded pool session", poolTier, localCode, remoteCode); this.recordMetrics(); // Convenience references const session = this._online[remoteCode].session; const cache = this._online[remoteCode].cache; // Reset the session timeout to leave the user with a full allotment of time session.resetTimeout(); // Send payload upstream session.sendMessage("user-info", content); // Forward future messages cache.on("enqueue", () => { const message = cache.dequeue(); this.emit("message", remoteCode, message[0], message[1]); }); // Save the start time to keep a record of the time spent on flavor servers var startTime = new Date().valueOf(); // Create touch interval for Redis and save reference const touchInterval = setInterval(() => { this.emit("touch", remoteCode, startTime); }, config.redis.expire.interval); // Emit an event to set to live in Redis (required for OT) this.emit("live", remoteCode); // Add user and touchInterval to store this._online[remoteCode].user = user; this._online[remoteCode].touchInterval = touchInterval; // Flush cached messages // Do this at the end in case any of the messages are "exit" messages while (!cache.isEmpty()) { const message = cache.dequeue(); mlog.trace("Flushing message:", remoteCode, message[0]); if (/exit/.test(message[0])) log.warn("Exit message:", message[1]); this.emit("message", remoteCode, message[0], message[1]); } } get(sessCode) { // Look up session const meta = this._online[sessCode]; if (!meta) return null; // Return it return meta.session; } destroy(sessCode, reason) { // Look up session const meta = this._online[sessCode]; if (!meta) { if (/Shell Exited/.test(reason)) return; else return log.warn("Cannot find session to destroy:", sessCode, reason); } // Destroy the session const session = meta.session; session.destroy((err) => { if (err) { log.error("Error destroying session:", sessCode, err); } this.emit("destroy-done", sessCode); }, reason); // Send destroy-u message this.emit("destroy-u", sessCode, reason); // Dereference pointers meta.session = null; meta.cache = null; meta.user = null; clearInterval(meta.touchInterval); // Remove it from the index delete this._online[sessCode]; log.debug("Removed session from index", sessCode); this.recordMetrics(); } startPool() { if (this._poolVar && this._poolVar.enabled) { throw new Error("Another pool is already running"); } // I made poolVar a local variable so that there will be one instance for each startPool closure. Each pool creation loop should be independent from any other pool creation loops that might start or stop. (Potential problem that this approach prevents: if a session is in the middle of creating, and the pool is disabled and then immediately enabled again, the first pool might not be destroyed if the "pool enabled" variable were singleton.) let poolVar = { enabled: true }; this._poolVar = poolVar; let _poolCb = (localCode, session, cache, options) => { // If we need to disable the pool... if (!poolVar.enabled) { if (session) session.destroy(null, "Pool Disabled"); return; } // If the session was created successfully... if (session) { if (!this._monitor_session) { log.info("Created monitor session:", localCode); this._monitor_session = session; cache.enabled = false; cache.removeAll(); } else { this._pool[options.tier][localCode] = { session, cache }; this.recordMetrics(); } } // If we need to put another session in our pool... for (let tier of Object.keys(this._pool)) { if (Object.keys(this._pool[tier]).length < this._poolSizes[tier]) { log.trace("Creating new session in tier", tier); this._create(_poolCb, { tier }); return; } } // No more sessions were required. setTimeout(_poolCb, config.sessionManager.poolInterval); }; process.nextTick(_poolCb); } disablePool() { if (!this._poolVar || !this._poolVar.enabled) return; this._poolVar.enabled = false; Object.keys(this._pool).forEach((tier) => { Object.keys(this._pool[tier]).forEach((localCode) => { this._pool[tier][localCode].session.destroy(null, "Pool Disabled"); this._pool[tier][localCode].session = null; this._pool[tier][localCode].cache = null; delete this._pool[tier][localCode]; }); }); this.recordMetrics(); } terminate(reason) { this.disablePool(); clearInterval(this._logInterval); clearInterval(this._keepAliveInterval); if (this._monitor_session) { this._monitor_session.destroy(null, reason); this._monitor_session = null; } Object.keys(this._online).forEach((remoteCode) => { this.destroy(remoteCode, reason); }); } restart() { this.startPool(); this._setup(); } recordMetrics() { metrics.gauge("oo.online_sessions", Object.keys(this._online).length); Object.keys(this._pool).forEach((tier) => { metrics.gauge(`oo.pool_sessions.${tier}`, Object.keys(this._pool[tier]).length); }); } } module.exports = SessionManager; ================================================ FILE: back-octave/Makefile ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . CC = gcc CFLAGS = -Wall -pthread -I/usr/local/include -L/usr/local/lib DEPS = OBJ = host.o LIBS = -luv -ljson-c EXE = octave-host %.o: %.c $(DEPS) $(CC) -c -o $@ $< $(CFLAGS) $(EXE): $(OBJ) $(CC) -o $@ $^ $(CFLAGS) $(LIBS) install: $(EXE) cp $(EXE) /usr/local/bin clean: rm $(OBJ) $(EXE) run: $(EXE) ./$(EXE) ================================================ FILE: back-octave/README.md ================================================ Octave Online Server: GNU Octave Utilities ========================================== This directory contains machinery for talking to the GNU Octave process. See [containers/README.md](../containers/README.md) for instructions on how to build the custom GNU Octave for Octave Online Server. The *oo-changesets* directory contains patches against GNU Octave to add features required for Octave Online. The primary feature added is a new flag `--json-sock`, which uses a UNIX Socket for passing messages back and forth between the Octave process and the outside world. The file *host.c* is a thin GNU Octave wrapper process that creates the UNIX Socket for talking to GNU Octave. It reads messages from STDIN and marshals them into the UNIX Socket, and when it receives a message from the socket, it prints the message to STDOUT. The *Makefile* is used for building *host.c* into an executable. The file *octaverc.m* is the default site-wide octaverc file for Octave Online Server. It is installed either into the Docker instance or into the current local server via the *install-site-m* make target in the top-level directory. ================================================ FILE: back-octave/host.c ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ #include #include #include #include #include #include #include // MACROS, CONSTANTS, GLOBALS, AND TYPEDEFS int r; #define CATCH(expr){ \ r = expr; \ if(r){ \ fprintf(stderr, "@ %s (%d): ", __FILE__, __LINE__); \ fprintf(stderr, "%d: %s\n", r, uv_strerror(r)); \ fflush(stderr); \ return r; \ } \ } #define LOG(...){ \ fprintf(stderr, __VA_ARGS__); \ fprintf(stderr, "\n"); \ fflush(stderr); \ } #define TMP_DIR_STRLEN 14 #define TMP_DIR_FORMAT "/tmp/octXXXXXX" #define TMP_COM_STRLEN 19 #define TMP_COM_FORMAT "/tmp/octXXXXXX/sock" uv_loop_t* loop = NULL; uv_tcp_t* sock_client = NULL; uv_process_t child_req; uv_process_options_t options; char* tmp_path; char* com_path; uv_pipe_t worker_com_p; uv_pipe_t worker_out_p; uv_pipe_t worker_err_p; uv_pipe_t host_in_p; uv_pipe_t host_out_p; uv_signal_t sigint_s; uv_signal_t sigterm_s; uv_signal_t sighup_s; typedef struct { uv_write_t req; uv_buf_t buf; } write_req_t; enum STD_STREAM { STD_STREAM_SOCKET_OUT, STD_STREAM_SOCKET_ERR, STD_STREAM_HOST_IN, STD_STREAM_SOCKET }; // UTILITY CALLBACKS void cb_walk(uv_handle_t* handle, void* arg) { uv_close(handle, NULL); } void cleanup() { // Close all remaining file descriptors uv_walk(loop, cb_walk, NULL); // End the loop uv_loop_close(loop); } void cb_cleanup_write_req(uv_write_t *req, int status) { write_req_t *wr = (write_req_t*) req; free(wr->buf.base); free(wr); } void cb_alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { *buf = uv_buf_init((char*) malloc(suggested_size), suggested_size); } void cb_sigfwd(uv_signal_t *handle, int signum) { LOG("Forwarding signal %d", signum); kill(child_req.pid, signum); } void cb_exit(uv_process_t* req, int64_t exit_status, int term_signal) { LOG("Process exited with status %" PRId64 ", signal %d", exit_status, term_signal); cleanup(); } // UTILITY FUNCTIONS // write_to_socket and write_to_stdout copy the memory so that other parts of the program cannot asynchronously mess with the writing process. void write_to_socket(const char* str, size_t len) { write_req_t* req = malloc(sizeof(write_req_t)); req->buf = uv_buf_init((char*) malloc(len), len); memcpy(req->buf.base, str, len); uv_write((uv_write_t*) req, (uv_stream_t*) sock_client, &req->buf, 1, cb_cleanup_write_req); } void write_to_stdout(const char* str, size_t len) { write_req_t* req = malloc(sizeof(write_req_t)); req->buf = uv_buf_init((char*) malloc(len), len); memcpy(req->buf.base, str, len); uv_write((uv_write_t*) req, (uv_stream_t*) &host_out_p, &req->buf, 1, cb_cleanup_write_req); } void print_json_msg_str(const char* name, const char* str, size_t len) { // Make the object json_object* nameobj = json_object_new_string(name); json_object* strobj = json_object_new_string_len(str, len); json_object* obj = json_object_new_array(); json_object_array_add(obj, nameobj); json_object_array_add(obj, strobj); // Make the string and print it const char* jstr = json_object_to_json_string(obj); write_to_stdout(jstr, strlen(jstr)); // Relese memory // jstr is automatically released along with obj: https://github.com/json-c/json-c/issues/83 json_object_put(obj); } // Process all messages received from the streams: child out, child err, stdin, and socket. // - "nread" is the number of bytes in the output. // - "buf" is the buffer, which was originally created by "alloc_buffer". // It may be longer than "nread". void process_std_stream(enum STD_STREAM type, uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { // Is the stream closed? if (nread < 0) { if (nread == UV_EOF) { uv_close((uv_handle_t*) stream, NULL); } } // What to do with the data? else if (nread > 0) { switch (type) { case STD_STREAM_SOCKET_OUT: print_json_msg_str("out", buf->base, nread); break; case STD_STREAM_SOCKET_ERR: print_json_msg_str("err", buf->base, nread); break; case STD_STREAM_HOST_IN: write_to_socket(buf->base, nread); break; case STD_STREAM_SOCKET: write_to_stdout(buf->base, nread); break; default: break; } } // Free memory (corresponding malloc: cb_alloc_buffer) if (buf->base) free(buf->base); } // MAIN CALLBACKS void cb_stdmsg(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { process_std_stream(STD_STREAM_SOCKET, stream, nread, buf); } void cb_stdout(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { process_std_stream(STD_STREAM_SOCKET_OUT, stream, nread, buf); } void cb_stderr(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { process_std_stream(STD_STREAM_SOCKET_ERR, stream, nread, buf); } void cb_stdin(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { process_std_stream(STD_STREAM_HOST_IN, stream, nread, buf); } void cb_connect(uv_stream_t* comm, int status) { if (status == -1) return; LOG("Connection received"); // Ignore connection if we already have one if (sock_client != NULL) return; sock_client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t)); uv_tcp_init(loop, sock_client); if (uv_accept(comm, (uv_stream_t*) sock_client) == 0) { uv_read_start((uv_stream_t*) sock_client, cb_alloc_buffer, cb_stdmsg); } else { uv_close((uv_handle_t*) sock_client, NULL); sock_client = NULL; } } // MAIN FUNCTION int main(int argc, char* argv[]) { signal(SIGPIPE, SIG_IGN); loop = uv_default_loop(); tmp_path = malloc(TMP_DIR_STRLEN); strcpy(tmp_path, TMP_DIR_FORMAT); com_path = malloc(TMP_COM_STRLEN); strcpy(com_path, TMP_COM_FORMAT); mkdtemp(tmp_path); memcpy(com_path, tmp_path, TMP_DIR_STRLEN); LOG("tmpdir: %s", com_path); // FIXME: Delete the temp dir before the process exits. CATCH(uv_pipe_init(loop, &worker_com_p, 0)); CATCH(uv_pipe_init(loop, &worker_out_p, 0)); CATCH(uv_pipe_init(loop, &worker_err_p, 0)); CATCH(uv_pipe_init(loop, &host_in_p, 0)); CATCH(uv_pipe_init(loop, &host_out_p, 0)); CATCH(uv_pipe_open(&host_in_p, 0)); CATCH(uv_pipe_open(&host_out_p, 1)); CATCH(uv_signal_init(loop, &sigint_s)); CATCH(uv_signal_init(loop, &sigterm_s)); CATCH(uv_signal_init(loop, &sighup_s)); // Let the command line argument translate into "--json-max-len" char* args[9]; args[0] = "octave"; args[1] = "--json-sock"; args[2] = com_path; args[3] = "--interactive"; args[4] = "--quiet"; args[5] = "--no-window-system"; args[6] = "--json-max-len"; if (argc > 1) { args[7] = argv[1]; } else { args[7] = "0"; } args[8] = NULL; options.exit_cb = cb_exit; options.file = "/usr/local/bin/octave"; // options.file = "/vagrant/octave/octave/build-no-docs/run-octave"; options.args = args; options.stdio_count = 3; uv_stdio_container_t child_stdio[3]; child_stdio[0].flags = UV_IGNORE; child_stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; child_stdio[1].data.stream = (uv_stream_t*) &worker_out_p; child_stdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; child_stdio[2].data.stream = (uv_stream_t*) &worker_err_p; options.stdio = child_stdio; CATCH(uv_spawn(loop, &child_req, &options)); CATCH(uv_pipe_bind(&worker_com_p, com_path)); CATCH(uv_listen((uv_stream_t*) &worker_com_p, 128, cb_connect)); CATCH(uv_read_start((uv_stream_t*) &worker_out_p, cb_alloc_buffer, cb_stdout)); CATCH(uv_read_start((uv_stream_t*) &worker_err_p, cb_alloc_buffer, cb_stderr)); CATCH(uv_read_start((uv_stream_t*) &host_in_p, cb_alloc_buffer, cb_stdin)); CATCH(uv_signal_start(&sigint_s, cb_sigfwd, SIGINT)); CATCH(uv_signal_start(&sigterm_s, cb_sigfwd, SIGTERM)); CATCH(uv_signal_start(&sighup_s, cb_sigfwd, SIGHUP)); LOG("Launched process with ID %d", child_req.pid); free(tmp_path); free(com_path); return uv_run(loop, UV_RUN_DEFAULT); } ================================================ FILE: back-octave/oo-changesets/000-README.md ================================================ Patch files from `001` until `100` are based on commit `323e92c4589f` in the GNU Octave mercurial repository, which is in the stable branch of version *4.1rc*. ================================================ FILE: back-octave/oo-changesets/001-d38b7c534496.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453104462 21600 # Mon Jan 18 02:07:42 2016 -0600 # Branch oo # Node ID d38b7c534496ca887f197901cf4fea75788ea579 # Parent 323e92c4589f7a47d67033e689e61dfa510cf911 Adding octave link binding "request_input". When enabled, the binding will be called whenever the user is prompted for input, and it should return a string corresponding to what the user entered in response. It should be enabled on an instance of octave_link by setting the private field _request_input_enabled=true. diff -r 323e92c4589f -r d38b7c534496 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Sat Jan 16 18:14:35 2016 -0800 +++ b/libinterp/corefcn/input.cc Mon Jan 18 02:07:42 2016 -0600 @@ -184,7 +184,11 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + if (octave_link::request_input_enabled ()) + retval = octave_link::request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; diff -r 323e92c4589f -r d38b7c534496 libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Sat Jan 16 18:14:35 2016 -0800 +++ b/libinterp/corefcn/octave-link.h Mon Jan 18 02:07:42 2016 -0600 @@ -276,6 +276,13 @@ instance->do_post_input_event (); } + static std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->do_request_input (prompt) + : std::string (); + } + static void enter_debugger_event (const std::string& file, int line) { if (enabled ()) @@ -323,6 +330,11 @@ return instance_ok () ? instance->link_enabled : false; } + static bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + static bool show_preferences () { @@ -396,6 +408,9 @@ void do_entered_readline_hook (void) { } void do_finished_readline_hook (void) { } + bool _request_input_enabled; + virtual std::string do_request_input (const std::string&) = 0; + virtual bool do_confirm_shutdown (void) = 0; virtual bool do_exit (int status) = 0; ================================================ FILE: back-octave/oo-changesets/002-d3de6023e846.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453119478 21600 # Mon Jan 18 06:17:58 2016 -0600 # Branch oo # Node ID d3de6023e84666d9db08e00e3417d2601d05989e # Parent d38b7c534496ca887f197901cf4fea75788ea579 Adding "show_static_plot" endpoint in octave link and synchronizing with GNUPlot when creating output files upon redraw plot commands. diff -r d38b7c534496 -r d3de6023e846 libinterp/corefcn/octave-link.cc --- a/libinterp/corefcn/octave-link.cc Mon Jan 18 02:07:42 2016 -0600 +++ b/libinterp/corefcn/octave-link.cc Mon Jan 18 06:17:58 2016 -0600 @@ -448,5 +448,30 @@ return retval; } +DEFUN (__octave_link_plot_destination__, , , + "-*- texinfo -*-\n\ +@deftypefn {} {} __octave_link_plot_destination__ ()\n\ +Undocumented internal function.\n\ +@end deftypefn") +{ + return octave_value (octave_link::plot_destination ()); +} +DEFUN (__octave_link_show_static_plot__, args, , + "-*- texinfo -*-\n\ +@deftypefn {} {} __octave_link_show_static_plot__ (@var{term}, @var{content})\n\ +Undocumented internal function.\n\ +@end deftypefn") +{ + if (args.length () != 2) { + octave_value retval; return retval; // v4.0 + // return ovl (); // v4.1.0+ + } + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return octave_value (octave_link::show_static_plot (term, content)); +} + + + diff -r d38b7c534496 -r d3de6023e846 libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Mon Jan 18 02:07:42 2016 -0600 +++ b/libinterp/corefcn/octave-link.h Mon Jan 18 06:17:58 2016 -0600 @@ -335,6 +335,29 @@ return enabled () ? instance->_request_input_enabled : false; } + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + + static plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + static bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->do_show_static_plot (term, content); + return true; + } + else + return false; + } + static bool show_preferences () { @@ -488,7 +511,11 @@ virtual void do_show_preferences (void) = 0; - virtual void do_show_doc (const std::string &file) = 0; + virtual void do_show_doc (const std::string& file) = 0; + + plot_destination_t _plot_destination; + virtual void do_show_static_plot (const std::string& term, + const std::string& content) = 0; }; #endif // OCTAVELINK_H diff -r d38b7c534496 -r d3de6023e846 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Mon Jan 18 02:07:42 2016 -0600 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Mon Jan 18 06:17:58 2016 -0600 @@ -27,9 +27,75 @@ if (nargin < 1 || nargin > 5 || nargin == 2) print_usage (); + + elseif (nargin >= 3 && nargin <= 5) + ## Write the plot to the given file (e.g., via the "print" command) + __gnuplot_draw_to_file__ (h, term, file, mono, debug_file); + + elseif (nargin == 1) + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __octave_link_plot_destination__ () == 0 || + __octave_link_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __go_draw_figure__ (h, plot_stream(1), enhanced, mono); + fflush (plot_stream(1)); + endif + + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __octave_link_plot_destination__ () == 1 || + __octave_link_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + pause (1); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __octave_link_show_static_plot__ (term, a); + endif + endif + endif + endif +endfunction - if (nargin >= 3 && nargin <= 5) +function __gnuplot_draw_to_file__ (h, term, file, mono = false, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -65,45 +131,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __go_draw_figure__ (h, plot_stream(1), enhanced, mono); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif - endfunction function enhanced = gnuplot_set_term (plot_stream, new_stream, h, term, file) ================================================ FILE: back-octave/oo-changesets/003-4d28376c34a8.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453120386 21600 # Mon Jan 18 06:33:06 2016 -0600 # Branch oo # Node ID 4d28376c34a877ba9eb5aefb84282c5bdf72a277 # Parent d3de6023e84666d9db08e00e3417d2601d05989e Adding octave link endpoint clear_screen (for the "clc" command). diff -r d3de6023e846 -r 4d28376c34a8 libgui/src/octave-qt-link.cc --- a/libgui/src/octave-qt-link.cc Mon Jan 18 06:17:58 2016 -0600 +++ b/libgui/src/octave-qt-link.cc Mon Jan 18 06:33:06 2016 -0600 @@ -509,6 +509,11 @@ } void +octave_qt_link::do_clear_screen (void) +{ +} + +void octave_qt_link::do_pre_input_event (void) { } diff -r d3de6023e846 -r 4d28376c34a8 libgui/src/octave-qt-link.h --- a/libgui/src/octave-qt-link.h Mon Jan 18 06:17:58 2016 -0600 +++ b/libgui/src/octave-qt-link.h Mon Jan 18 06:33:06 2016 -0600 @@ -117,6 +117,8 @@ void do_append_history (const std::string& hist_entry); void do_clear_history (void); + void do_clear_screen (void); + void do_pre_input_event (void); void do_post_input_event (void); diff -r d3de6023e846 -r 4d28376c34a8 libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Mon Jan 18 06:17:58 2016 -0600 +++ b/libinterp/corefcn/octave-link.h Mon Jan 18 06:33:06 2016 -0600 @@ -264,6 +264,12 @@ instance->do_clear_history (); } + static void clear_screen (void) + { + if (enabled ()) + instance->do_clear_screen (); + } + static void pre_input_event (void) { if (enabled ()) @@ -492,6 +498,8 @@ virtual void do_append_history (const std::string& hist_entry) = 0; virtual void do_clear_history (void) = 0; + virtual void do_clear_screen (void) = 0; + virtual void do_pre_input_event (void) = 0; virtual void do_post_input_event (void) = 0; diff -r d3de6023e846 -r 4d28376c34a8 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Mon Jan 18 06:17:58 2016 -0600 +++ b/libinterp/corefcn/sysdep.cc Mon Jan 18 06:33:06 2016 -0600 @@ -75,6 +75,7 @@ #include "error.h" #include "input.h" #include "oct-obj.h" +#include "octave-link.h" #include "ov.h" #include "pager.h" #include "parse.h" @@ -596,6 +597,8 @@ { bool skip_redisplay = true; + octave_link::clear_screen (); + command_editor::clear_screen (skip_redisplay); return octave_value_list (); ================================================ FILE: back-octave/oo-changesets/004-6ff3e34eea77.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453120461 21600 # Mon Jan 18 06:34:21 2016 -0600 # Branch oo # Node ID 6ff3e34eea77bae35e07007cbd7a1a17e7fedacc # Parent 4d28376c34a877ba9eb5aefb84282c5bdf72a277 Adding support for condition variables to octave_mutex. diff -r 4d28376c34a8 -r 6ff3e34eea77 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Mon Jan 18 06:33:06 2016 -0600 +++ b/liboctave/util/oct-mutex.cc Mon Jan 18 06:34:21 2016 -0600 @@ -53,6 +53,18 @@ return false; } +void +octave_base_mutex::cond_wait (void) +{ + (*current_liboctave_error_handler) ("mutex not supported on this platform"); +} + +void +octave_base_mutex::cond_signal (void) +{ + (*current_liboctave_error_handler) ("mutex not supported on this platform"); +} + #if defined (__WIN32__) && ! defined (__CYGWIN__) class @@ -63,11 +75,13 @@ : octave_base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~octave_w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -85,8 +99,21 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + + void + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD octave_thread_id = 0; @@ -118,11 +145,18 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~octave_pthread_mutex (void) { pthread_mutex_destroy (&pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -140,8 +174,20 @@ return (pthread_mutex_trylock (&pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t pm; + pthread_cond_t condv; + }; static pthread_t octave_thread_id = 0; diff -r 4d28376c34a8 -r 6ff3e34eea77 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Mon Jan 18 06:33:06 2016 -0600 +++ b/liboctave/util/oct-mutex.h Mon Jan 18 06:34:21 2016 -0600 @@ -43,6 +43,10 @@ virtual bool try_lock (void); + virtual void cond_wait (void); + + virtual void cond_signal (void); + private: octave_refcount count; }; @@ -95,6 +99,16 @@ return rep->try_lock (); } + void cond_wait (void) + { + rep->cond_wait (); + } + + void cond_signal (void) + { + rep->cond_signal (); + } + protected: octave_base_mutex *rep; }; ================================================ FILE: back-octave/oo-changesets/005-9e73fe0d92d5.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453121851 21600 # Mon Jan 18 06:57:31 2016 -0600 # Branch oo # Node ID 9e73fe0d92d5817002f67034983af5b557f874be # Parent 6ff3e34eea77bae35e07007cbd7a1a17e7fedacc Changing definition of "isguirunning()" to "octave_link::enabled()". This enables triggering octave link endpoints by implementations of octave link other than the GUI. diff -r 6ff3e34eea77 -r 9e73fe0d92d5 libinterp/octave.cc --- a/libinterp/octave.cc Mon Jan 18 06:34:21 2016 -0600 +++ b/libinterp/octave.cc Mon Jan 18 06:57:31 2016 -0600 @@ -65,6 +65,7 @@ #include "oct-map.h" #include "oct-mutex.h" #include "oct-obj.h" +#include "octave-link.h" #include "ops.h" #include "options-usage.h" #include "ov.h" @@ -989,7 +990,7 @@ octave_value retval; if (args.length () == 0) - retval = start_gui; + retval = octave_link::enabled (); else print_usage (); ================================================ FILE: back-octave/oo-changesets/006-15d21ceec728.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453127653 21600 # Mon Jan 18 08:34:13 2016 -0600 # Branch oo # Node ID 15d21ceec7282b94db2d86503127d03730d5c316 # Parent 9e73fe0d92d5817002f67034983af5b557f874be Adding a "--json-sock" command line option. With this option, Octave will publish all octave link messages as JSON objects to the specified UNIX domain socket. diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Mon Jan 18 08:34:13 2016 -0600 @@ -0,0 +1,79 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(const std::string& json_sock_path) + : _json_sock_path (json_sock_path), + _loop_thread_active (false), + _octave_json_link (this) +{ + // Enable octave_json_link instance + octave_link::connect_link(&_octave_json_link); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link.receive_message(name, jobj); +} diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Mon Jan 18 08:34:13 2016 -0600 @@ -0,0 +1,29 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +class json_main { +public: + json_main(const std::string& json_sock_path); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + octave_json_link _octave_json_link; +}; + +#endif diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Mon Jan 18 08:34:13 2016 -0600 @@ -0,0 +1,226 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + return json_object_new_string(str.c_str()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + typename std::list::const_iterator it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_workspace_list(const std::list& list) { + return json_object_from_list(list, json_util::from_workspace_element); +} + +JSON_OBJECT_T json_util::from_filter_list(const octave_link::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_workspace_element(workspace_element element) { + JSON_MAP_T m; + m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.symbol()); + m["class_name"] = json_util::from_string(element.class_name()); + m["dimension"] = json_util::from_string(element.dimension()); + m["value"] = json_util::from_string(element.value()); + m["complex_flag"] = json_util::from_boolean(element.complex_flag()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_object_new_string(pair.first.c_str())); + json_object_array_add(jobj, json_object_new_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (int i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Mon Jan 18 08:34:13 2016 -0600 @@ -0,0 +1,55 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "workspace-element.h" +#include "octave-link.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_workspace_list(const std::list& list); + static JSON_OBJECT_T from_filter_list(const octave_link::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_workspace_element(workspace_element element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +#endif diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Mon Jan 18 06:57:31 2016 -0600 +++ b/libinterp/corefcn/module.mk Mon Jan 18 08:34:13 2016 -0600 @@ -66,6 +66,8 @@ corefcn/help.h \ corefcn/hook-fcn.h \ corefcn/input.h \ + corefcn/json-main.h \ + corefcn/json-util.h \ corefcn/load-path.h \ corefcn/load-save.h \ corefcn/ls-ascii-helper.h \ @@ -95,6 +97,7 @@ corefcn/oct-strstrm.h \ corefcn/oct.h \ corefcn/octave-default-image.h \ + corefcn/octave-json-link.h \ corefcn/octave-link.h \ corefcn/pager.h \ corefcn/pr-output.h \ @@ -192,6 +195,8 @@ corefcn/hex2num.cc \ corefcn/hook-fcn.cc \ corefcn/input.cc \ + corefcn/json-main.cc \ + corefcn/json-util.cc \ corefcn/inv.cc \ corefcn/kron.cc \ corefcn/load-path.cc \ @@ -226,6 +231,7 @@ corefcn/oct-procbuf.cc \ corefcn/oct-stream.cc \ corefcn/oct-strstrm.cc \ + corefcn/octave-json-link.cc \ corefcn/octave-link.cc \ corefcn/ordschur.cc \ corefcn/pager.cc \ diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Mon Jan 18 08:34:13 2016 -0600 @@ -0,0 +1,338 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "workspace-element.h" +#include "json-main.h" + +// MAP_SET(m, foo, string) +// => m["foo"] = json_util::from_string(foo); +#define MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +octave_json_link::octave_json_link(json_main* __json_main) + : octave_link (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::do_request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + _publish_message("request-input", json_util::from_string(prompt)); + + return request_input_queue.dequeue(); +} + +bool octave_json_link::do_confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +bool octave_json_link::do_exit(int status) { + JSON_MAP_T m; + MAP_SET(m, status, int); + _publish_message("exit", json_util::from_map(m)); + + // It is our responsibility in octave_link to call exit. If we don't, then + // the kernel waits for 24 hours expecting us to do something. + ::exit(status); + + return true; +} + +bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { + // Triggered in "msgbox", "helpdlg", and "errordlg", among others + JSON_MAP_T m; + MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); + MAP_SET(m, msg, string); + MAP_SET(m, title, string); + _publish_message("message-dialog", json_util::from_map(m)); + + return message_dialog_queue.dequeue(); +} + +std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + MAP_SET(m, msg, string); + MAP_SET(m, title, string); + MAP_SET(m, btn1, string); + MAP_SET(m, btn2, string); + MAP_SET(m, btn3, string); + MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::do_list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + MAP_SET(m, list, string_list); + MAP_SET(m, mode, string); + MAP_SET(m, width, int); + MAP_SET(m, height, int); + MAP_SET(m, initial_value, int_list); + MAP_SET(m, name, string); + MAP_SET(m, prompt, string_list); + MAP_SET(m, ok_string, string); + MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + MAP_SET(m, prompt, string_list); + MAP_SET(m, title, string); + MAP_SET(m, nr, float_list); + MAP_SET(m, nc, float_list); + MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + MAP_SET(m, filter, filter_list); + MAP_SET(m, title, string); + MAP_SET(m, filename, string); + MAP_SET(m, pathname, string); + MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::do_debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + MAP_SET(m, file, string); + MAP_SET(m, dir, string); + MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::do_change_directory(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::do_execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +void octave_json_link::do_set_workspace(bool top_level, bool debug, const std::list& ws) { + // Triggered on every new line entry + JSON_MAP_T m; + MAP_SET(m, top_level, boolean); + MAP_SET(m, debug, boolean); + MAP_SET(m, ws, workspace_list); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::do_clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::do_set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::do_append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::do_clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::do_clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::do_pre_input_event(void) { + // noop +} + +void octave_json_link::do_post_input_event(void) { + // noop +} + +void octave_json_link::do_enter_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + MAP_SET(m, file, string); + MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + MAP_SET(m, file, string); + MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::do_update_breakpoint(bool insert, const std::string& file, int line) { + JSON_MAP_T m; + MAP_SET(m, insert, boolean); + MAP_SET(m, file, string); + MAP_SET(m, line, int); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { + // Triggered upon interpreter startup + JSON_MAP_T m; + MAP_SET(m, ps1, string); + MAP_SET(m, ps2, string); + MAP_SET(m, ps4, string); + _publish_message("set-default-prompts", json_util::from_map(m)); +} + +void octave_json_link::do_show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +void octave_json_link::do_show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + JSON_MAP_T m; + MAP_SET(m, term, string); + MAP_SET(m, content, string); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Mon Jan 18 08:34:13 2016 -0600 @@ -0,0 +1,185 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "octave-link.h" +#include "json-util.h" +#include "oct-mutex.h" + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + +private: + std::queue _queue; + octave_mutex _mutex; +}; + +class octave_json_link : public octave_link +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string do_request_input (const std::string& prompt); + + bool do_confirm_shutdown (void); + bool do_exit (int status); + + bool do_copy_image_to_clipboard (const std::string& file); + + bool do_edit_file (const std::string& file); + bool do_prompt_new_edit_file (const std::string& file); + + int do_message_dialog (const std::string& dlg, const std::string& msg, + const std::string& title); + + std::string + do_question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef); + + std::pair, int> + do_list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string); + + std::list + do_input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults); + + std::list + do_file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode); + + int + do_debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option); + + void do_change_directory (const std::string& dir); + + void do_execute_command_in_terminal (const std::string& command); + + void do_set_workspace (bool top_level, bool debug, + const std::list& ws); + + void do_clear_workspace (void); + + void do_set_history (const string_vector& hist); + void do_append_history (const std::string& hist_entry); + void do_clear_history (void); + + void do_clear_screen (void); + + void do_pre_input_event (void); + void do_post_input_event (void); + + void do_enter_debugger_event (const std::string& file, int line); + void do_execute_in_debugger_event (const std::string& file, int line); + void do_exit_debugger_event (void); + + void do_update_breakpoint (bool insert, const std::string& file, int line); + + void do_set_default_prompts (std::string& ps1, std::string& ps2, + std::string& ps4); + + void do_show_preferences (void); + + void do_show_doc (const std::string& file); + + void do_show_static_plot (const std::string& term, + const std::string& content); + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +#endif diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/link-deps.mk --- a/libinterp/link-deps.mk Mon Jan 18 06:57:31 2016 -0600 +++ b/libinterp/link-deps.mk Mon Jan 18 08:34:13 2016 -0600 @@ -17,7 +17,8 @@ $(GL2PS_LIBS) \ $(LLVM_LIBS) \ $(JAVA_LIBS) \ - $(LAPACK_LIBS) + $(LAPACK_LIBS) \ + -ljson-c LIBOCTINTERP_LINK_OPTS = \ $(FT2_LDFLAGS) \ diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/octave.cc --- a/libinterp/octave.cc Mon Jan 18 06:57:31 2016 -0600 +++ b/libinterp/octave.cc Mon Jan 18 08:34:13 2016 -0600 @@ -56,6 +56,7 @@ #include "file-io.h" #include "help.h" #include "input.h" +#include "json-main.h" #include "lex.h" #include "load-path.h" #include "load-save.h" @@ -154,6 +155,10 @@ // (--image-path) static std::string image_path; +// The value for "JSON_SOCK" specified on the command line. +// (--json-sock) +static std::string json_sock_path; + // If TRUE, ignore the window system even if it is available. // (--no-window-system, -W) static bool no_window_system = false; @@ -657,6 +662,11 @@ forced_line_editing = true; break; + case JSON_SOCK_OPTION: + if (optarg) + json_sock_path = optarg; + break; + case NO_GUI_OPTION: no_gui_option = true; break; @@ -832,6 +842,11 @@ initialize_version_info (); + if (!json_sock_path.empty ()) { + static json_main _json_main (json_sock_path); + _json_main.run_loop_on_new_thread(); + } + // Make all command-line arguments available to startup files, // including PKG_ADD files. diff -r 9e73fe0d92d5 -r 15d21ceec728 libinterp/options-usage.h --- a/libinterp/options-usage.h Mon Jan 18 06:57:31 2016 -0600 +++ b/libinterp/options-usage.h Mon Jan 18 08:34:13 2016 -0600 @@ -33,8 +33,8 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--force-gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--line-editing] [--no-gui] [--no-history]\n\ - [--no-init-file] [--no-init-path] [--no-line-editing]\n\ + [--jit-compiler] [--json-sock] [--line-editing] [--no-gui]\n\ + [--no-history][--no-init-file] [--no-init-path] [--no-line-editing]\n\ [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; @@ -56,15 +56,16 @@ #define INFO_PROG_OPTION 8 #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 -#define LINE_EDITING_OPTION 11 -#define NO_GUI_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define TEXI_MACROS_FILE_OPTION 18 -#define TRADITIONAL_OPTION 19 +#define JSON_SOCK_OPTION 11 +#define LINE_EDITING_OPTION 12 +#define NO_GUI_OPTION 13 +#define NO_INIT_FILE_OPTION 14 +#define NO_INIT_PATH_OPTION 15 +#define NO_LINE_EDITING_OPTION 16 +#define NO_SITE_FILE_OPTION 17 +#define PERSIST_OPTION 18 +#define TEXI_MACROS_FILE_OPTION 19 +#define TRADITIONAL_OPTION 20 struct option long_opts[] = { { "braindead", no_argument, 0, TRADITIONAL_OPTION }, @@ -82,6 +83,7 @@ { "info-program", required_argument, 0, INFO_PROG_OPTION }, { "interactive", no_argument, 0, 'i' }, { "jit-compiler", no_argument, 0, JIT_COMPILER_OPTION }, + { "json-sock", required_argument, 0, JSON_SOCK_OPTION }, { "line-editing", no_argument, 0, LINE_EDITING_OPTION }, { "no-gui", no_argument, 0, NO_GUI_OPTION }, { "no-history", no_argument, 0, 'H' }, @@ -128,6 +130,7 @@ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ ================================================ FILE: back-octave/oo-changesets/007-4d778d6ebbd0.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1453275208 21600 # Wed Jan 20 01:33:28 2016 -0600 # Branch oo # Node ID 4d778d6ebbd04afad8886efb7770690fbba675a9 # Parent 15d21ceec7282b94db2d86503127d03730d5c316 Fixing undefined reference error when DISPLAY is enabled. diff -r 15d21ceec728 -r 4d778d6ebbd0 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Mon Jan 18 08:34:13 2016 -0600 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Wed Jan 20 01:33:28 2016 -0600 @@ -30,7 +30,11 @@ elseif (nargin >= 3 && nargin <= 5) ## Write the plot to the given file (e.g., via the "print" command) - __gnuplot_draw_to_file__ (h, term, file, mono, debug_file); + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, mono, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file, mono); + endif elseif (nargin == 1) ## Plot to terminal and/or static (e.g., via the "plot" command) ================================================ FILE: back-octave/oo-changesets/008-e8ef7f3333bf.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1458303601 18000 # Fri Mar 18 07:20:01 2016 -0500 # Branch oo # Node ID e8ef7f3333bfd96d811bde8cbc68738f37b191a6 # Parent 4d778d6ebbd04afad8886efb7770690fbba675a9 Moving macro "MAP_SET" into json-util.h and renaming it to "JSON_MAP_SET" diff -r 4d778d6ebbd0 -r e8ef7f3333bf libinterp/corefcn/json-util.h --- a/libinterp/corefcn/json-util.h Wed Jan 20 01:33:28 2016 -0600 +++ b/libinterp/corefcn/json-util.h Fri Mar 18 07:20:01 2016 -0500 @@ -17,6 +17,10 @@ #define JSON_OBJECT_T json_object* #define JSON_MAP_T std::map +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + class json_util { public: static JSON_OBJECT_T from_string(const std::string& str); diff -r 4d778d6ebbd0 -r e8ef7f3333bf libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Wed Jan 20 01:33:28 2016 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Fri Mar 18 07:20:01 2016 -0500 @@ -28,12 +28,7 @@ #include "octave-json-link.h" #include "workspace-element.h" #include "json-main.h" - -// MAP_SET(m, foo, string) -// => m["foo"] = json_util::from_string(foo); -#define MAP_SET(M, FIELD, TYPE){ \ - m[#FIELD] = json_util::from_##TYPE (FIELD); \ -} +#include "json-util.h" octave_json_link::octave_json_link(json_main* __json_main) : octave_link (), @@ -61,7 +56,7 @@ bool octave_json_link::do_exit(int status) { JSON_MAP_T m; - MAP_SET(m, status, int); + JSON_MAP_SET(m, status, int); _publish_message("exit", json_util::from_map(m)); // It is our responsibility in octave_link to call exit. If we don't, then @@ -74,7 +69,7 @@ bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; - MAP_SET(m, file, string); + JSON_MAP_SET(m, file, string); _publish_message("copy-image-to-clipboard", json_util::from_map(m)); return true; @@ -83,7 +78,7 @@ bool octave_json_link::do_edit_file(const std::string& file) { // Triggered in "edit" for existing files JSON_MAP_T m; - MAP_SET(m, file, string); + JSON_MAP_SET(m, file, string); _publish_message("edit-file", json_util::from_map(m)); return true; @@ -92,7 +87,7 @@ bool octave_json_link::do_prompt_new_edit_file(const std::string& file) { // Triggered in "edit" for new files JSON_MAP_T m; - MAP_SET(m, file, string); + JSON_MAP_SET(m, file, string); _publish_message("prompt-new-edit-file", json_util::from_map(m)); return prompt_new_edit_file_queue.dequeue(); @@ -101,9 +96,9 @@ int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { // Triggered in "msgbox", "helpdlg", and "errordlg", among others JSON_MAP_T m; - MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); - MAP_SET(m, msg, string); - MAP_SET(m, title, string); + JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); _publish_message("message-dialog", json_util::from_map(m)); return message_dialog_queue.dequeue(); @@ -112,12 +107,12 @@ std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { // Triggered in "questdlg" JSON_MAP_T m; - MAP_SET(m, msg, string); - MAP_SET(m, title, string); - MAP_SET(m, btn1, string); - MAP_SET(m, btn2, string); - MAP_SET(m, btn3, string); - MAP_SET(m, btndef, string); + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); _publish_message("question-dialog", json_util::from_map(m)); return question_dialog_queue.dequeue(); @@ -126,15 +121,15 @@ std::pair, int> octave_json_link::do_list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { // Triggered in "listdlg" JSON_MAP_T m; - MAP_SET(m, list, string_list); - MAP_SET(m, mode, string); - MAP_SET(m, width, int); - MAP_SET(m, height, int); - MAP_SET(m, initial_value, int_list); - MAP_SET(m, name, string); - MAP_SET(m, prompt, string_list); - MAP_SET(m, ok_string, string); - MAP_SET(m, cancel_string, string); + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); _publish_message("list-dialog", json_util::from_map(m)); return list_dialog_queue.dequeue(); @@ -143,11 +138,11 @@ std::list octave_json_link::do_input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { // Triggered in "inputdlg" JSON_MAP_T m; - MAP_SET(m, prompt, string_list); - MAP_SET(m, title, string); - MAP_SET(m, nr, float_list); - MAP_SET(m, nc, float_list); - MAP_SET(m, defaults, string_list); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); _publish_message("input-dialog", json_util::from_map(m)); return input_dialog_queue.dequeue(); @@ -156,11 +151,11 @@ std::list octave_json_link::do_file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { // Triggered in "uiputfile", "uigetfile", and "uigetdir" JSON_MAP_T m; - MAP_SET(m, filter, filter_list); - MAP_SET(m, title, string); - MAP_SET(m, filename, string); - MAP_SET(m, pathname, string); - MAP_SET(m, multimode, string); + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); _publish_message("file-dialog", json_util::from_map(m)); return file_dialog_queue.dequeue(); @@ -169,9 +164,9 @@ int octave_json_link::do_debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { // This endpoint might be unused? (No references) JSON_MAP_T m; - MAP_SET(m, file, string); - MAP_SET(m, dir, string); - MAP_SET(m, addpath_option, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); return debug_cd_or_addpath_error_queue.dequeue(); @@ -180,23 +175,23 @@ void octave_json_link::do_change_directory(const std::string& dir) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; - MAP_SET(m, dir, string); + JSON_MAP_SET(m, dir, string); _publish_message("change-directory", json_util::from_map(m)); } void octave_json_link::do_execute_command_in_terminal(const std::string& command) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; - MAP_SET(m, command, string); + JSON_MAP_SET(m, command, string); _publish_message("execute-command-in-terminal", json_util::from_map(m)); } void octave_json_link::do_set_workspace(bool top_level, bool debug, const std::list& ws) { // Triggered on every new line entry JSON_MAP_T m; - MAP_SET(m, top_level, boolean); - MAP_SET(m, debug, boolean); - MAP_SET(m, ws, workspace_list); + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, workspace_list); _publish_message("set-workspace", json_util::from_map(m)); } @@ -208,14 +203,14 @@ void octave_json_link::do_set_history(const string_vector& hist) { // Called at startup, possibly more? JSON_MAP_T m; - MAP_SET(m, hist, string_vector); + JSON_MAP_SET(m, hist, string_vector); _publish_message("set-history", json_util::from_map(m)); } void octave_json_link::do_append_history(const std::string& hist_entry) { // Appears to be tied to readline, if available JSON_MAP_T m; - MAP_SET(m, hist_entry, string); + JSON_MAP_SET(m, hist_entry, string); _publish_message("append-history", json_util::from_map(m)); } @@ -239,15 +234,15 @@ void octave_json_link::do_enter_debugger_event(const std::string& file, int line) { JSON_MAP_T m; - MAP_SET(m, file, string); - MAP_SET(m, line, int); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); _publish_message("enter-debugger-event", json_util::from_map(m)); } void octave_json_link::do_execute_in_debugger_event(const std::string& file, int line) { JSON_MAP_T m; - MAP_SET(m, file, string); - MAP_SET(m, line, int); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); _publish_message("execute-in-debugger-event", json_util::from_map(m)); } @@ -257,18 +252,18 @@ void octave_json_link::do_update_breakpoint(bool insert, const std::string& file, int line) { JSON_MAP_T m; - MAP_SET(m, insert, boolean); - MAP_SET(m, file, string); - MAP_SET(m, line, int); + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); _publish_message("update-breakpoint", json_util::from_map(m)); } void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { // Triggered upon interpreter startup JSON_MAP_T m; - MAP_SET(m, ps1, string); - MAP_SET(m, ps2, string); - MAP_SET(m, ps4, string); + JSON_MAP_SET(m, ps1, string); + JSON_MAP_SET(m, ps2, string); + JSON_MAP_SET(m, ps4, string); _publish_message("set-default-prompts", json_util::from_map(m)); } @@ -285,8 +280,8 @@ void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { // Triggered on all plot commands with setenv("GNUTERM","svg") JSON_MAP_T m; - MAP_SET(m, term, string); - MAP_SET(m, content, string); + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); _publish_message("show-static-plot", json_util::from_map(m)); } ================================================ FILE: back-octave/oo-changesets/009-05f7272c001e.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1458307337 18000 # Fri Mar 18 08:22:17 2016 -0500 # Branch oo # Node ID 05f7272c001e9be692baa7ad0d0f5b4d4b29c642 # Parent e8ef7f3333bfd96d811bde8cbc68738f37b191a6 Adding a new "--json-max-len" option that puts a limit on the size of messages pushed through the JSON socket. diff -r e8ef7f3333bf -r 05f7272c001e libinterp/corefcn/json-main.cc --- a/libinterp/corefcn/json-main.cc Fri Mar 18 07:20:01 2016 -0500 +++ b/libinterp/corefcn/json-main.cc Fri Mar 18 08:22:17 2016 -0500 @@ -24,8 +24,9 @@ _json_main->process_json_object(name, jobj); } -json_main::json_main(const std::string& json_sock_path) +json_main::json_main(const std::string& json_sock_path, int max_message_length) : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), _loop_thread_active (false), _octave_json_link (this) { @@ -52,6 +53,19 @@ void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + send(sockfd, jstr.c_str(), jstr.length(), 0); } diff -r e8ef7f3333bf -r 05f7272c001e libinterp/corefcn/json-main.h --- a/libinterp/corefcn/json-main.h Fri Mar 18 07:20:01 2016 -0500 +++ b/libinterp/corefcn/json-main.h Fri Mar 18 08:22:17 2016 -0500 @@ -10,7 +10,7 @@ class json_main { public: - json_main(const std::string& json_sock_path); + json_main(const std::string& json_sock_path, int max_message_length); ~json_main(void); void publish_message(const std::string& name, JSON_OBJECT_T jobj); @@ -20,6 +20,7 @@ private: std::string _json_sock_path; + int _max_message_length; int sockfd; bool _loop_thread_active; pthread_t _loop_thread; diff -r e8ef7f3333bf -r 05f7272c001e libinterp/octave.cc --- a/libinterp/octave.cc Fri Mar 18 07:20:01 2016 -0500 +++ b/libinterp/octave.cc Fri Mar 18 08:22:17 2016 -0500 @@ -159,6 +159,10 @@ // (--json-sock) static std::string json_sock_path; +// The maximum message length; valid only if "JSON_SOCK" is specified. +// (--json-max-len) +static int json_max_message_length = 0; + // If TRUE, ignore the window system even if it is available. // (--no-window-system, -W) static bool no_window_system = false; @@ -667,6 +671,11 @@ json_sock_path = optarg; break; + case JSON_MAX_LEN_OPTION: + if (optarg) + json_max_message_length = strtol(optarg, NULL, 10); + break; + case NO_GUI_OPTION: no_gui_option = true; break; @@ -843,7 +852,7 @@ initialize_version_info (); if (!json_sock_path.empty ()) { - static json_main _json_main (json_sock_path); + static json_main _json_main (json_sock_path, json_max_message_length); _json_main.run_loop_on_new_thread(); } diff -r e8ef7f3333bf -r 05f7272c001e libinterp/options-usage.h --- a/libinterp/options-usage.h Fri Mar 18 07:20:01 2016 -0500 +++ b/libinterp/options-usage.h Fri Mar 18 08:22:17 2016 -0500 @@ -33,10 +33,10 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--force-gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--json-sock] [--line-editing] [--no-gui]\n\ - [--no-history][--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--silent] [--traditional]\n\ + [--jit-compiler] [--json-sock] [--json-max-len] [--line-editing]\n\ + [--no-gui] [--no-history][--no-init-file] [--no-init-path]\n\ + [--no-line-editing] [--no-site-file] [--no-window-system] [--norc]\n\ + [-p path] [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; // This is here so that it's more likely that the usage message and @@ -57,15 +57,16 @@ #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 #define JSON_SOCK_OPTION 11 -#define LINE_EDITING_OPTION 12 -#define NO_GUI_OPTION 13 -#define NO_INIT_FILE_OPTION 14 -#define NO_INIT_PATH_OPTION 15 -#define NO_LINE_EDITING_OPTION 16 -#define NO_SITE_FILE_OPTION 17 -#define PERSIST_OPTION 18 -#define TEXI_MACROS_FILE_OPTION 19 -#define TRADITIONAL_OPTION 20 +#define JSON_MAX_LEN_OPTION 12 +#define LINE_EDITING_OPTION 13 +#define NO_GUI_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define TEXI_MACROS_FILE_OPTION 20 +#define TRADITIONAL_OPTION 21 struct option long_opts[] = { { "braindead", no_argument, 0, TRADITIONAL_OPTION }, @@ -84,6 +85,7 @@ { "interactive", no_argument, 0, 'i' }, { "jit-compiler", no_argument, 0, JIT_COMPILER_OPTION }, { "json-sock", required_argument, 0, JSON_SOCK_OPTION }, + { "json-max-len", required_argument, 0, JSON_MAX_LEN_OPTION }, { "line-editing", no_argument, 0, LINE_EDITING_OPTION }, { "no-gui", no_argument, 0, NO_GUI_OPTION }, { "no-history", no_argument, 0, 'H' }, @@ -131,6 +133,7 @@ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ ================================================ FILE: back-octave/oo-changesets/010-4a1afb661c55.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1458312433 18000 # Fri Mar 18 09:47:13 2016 -0500 # Branch oo # Node ID 4a1afb661c55f34994b53920f97c33ff3d03a27d # Parent 05f7272c001e9be692baa7ad0d0f5b4d4b29c642 Modifying code to not emit "request-input" events when there is a command waiting in the queue. diff -r 05f7272c001e -r 4a1afb661c55 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Fri Mar 18 08:22:17 2016 -0500 +++ b/libinterp/corefcn/octave-json-link.cc Fri Mar 18 09:47:13 2016 -0500 @@ -42,9 +42,13 @@ std::string octave_json_link::do_request_input(const std::string& prompt) { // Triggered whenever the console prompts for user input - _publish_message("request-input", json_util::from_string(prompt)); - return request_input_queue.dequeue(); + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; } bool octave_json_link::do_confirm_shutdown(void) { diff -r 05f7272c001e -r 4a1afb661c55 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Fri Mar 18 08:22:17 2016 -0500 +++ b/libinterp/corefcn/octave-json-link.h Fri Mar 18 09:47:13 2016 -0500 @@ -41,6 +41,7 @@ void enqueue(const T& value); T dequeue(); + bool dequeue_to(T* destination); private: std::queue _queue; @@ -182,4 +183,17 @@ return value; } +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + #endif ================================================ FILE: back-octave/oo-changesets/011-7327936fa23e.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1481411965 0 # Sat Dec 10 23:19:25 2016 +0000 # Branch oo # Node ID 7327936fa23e3070c748c8db6ef4d1a0fc473da1 # Parent 4a1afb661c55f34994b53920f97c33ff3d03a27d Adding support for a "request-url" JSON event. diff -r 4a1afb661c55 -r 7327936fa23e libinterp/corefcn/json-util.cc --- a/libinterp/corefcn/json-util.cc Fri Mar 18 09:47:13 2016 -0500 +++ b/libinterp/corefcn/json-util.cc Sat Dec 10 23:19:25 2016 +0000 @@ -152,6 +152,22 @@ return ret; } +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + std::list json_util::to_string_list(JSON_OBJECT_T jobj) { return json_object_to_list(jobj, json_util::to_string); } diff -r 4a1afb661c55 -r 7327936fa23e libinterp/corefcn/json-util.h --- a/libinterp/corefcn/json-util.h Fri Mar 18 09:47:13 2016 -0500 +++ b/libinterp/corefcn/json-util.h Sat Dec 10 23:19:25 2016 +0000 @@ -46,6 +46,7 @@ static std::string to_string(JSON_OBJECT_T jobj); static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); static std::list to_string_list(JSON_OBJECT_T jobj); static int to_int(JSON_OBJECT_T jobj); static bool to_boolean(JSON_OBJECT_T jobj); diff -r 4a1afb661c55 -r 7327936fa23e libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Fri Mar 18 09:47:13 2016 -0500 +++ b/libinterp/corefcn/octave-json-link.cc Sat Dec 10 23:19:25 2016 +0000 @@ -51,6 +51,20 @@ return value; } +std::string octave_json_link::do_request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + bool octave_json_link::do_confirm_shutdown(void) { // Triggered when the kernel tries to exit _publish_message("confirm-shutdown", json_util::empty()); @@ -294,6 +308,10 @@ std::string answer = json_util::to_string(jobj); request_input_queue.enqueue(answer); } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } else if (name == "confirm-shutdown-answer"){ bool answer = json_util::to_boolean(jobj); confirm_shutdown_queue.enqueue(answer); diff -r 4a1afb661c55 -r 7327936fa23e libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Fri Mar 18 09:47:13 2016 -0500 +++ b/libinterp/corefcn/octave-json-link.h Sat Dec 10 23:19:25 2016 +0000 @@ -58,6 +58,7 @@ ~octave_json_link (void); std::string do_request_input (const std::string& prompt); + std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); bool do_confirm_shutdown (void); bool do_exit (int status); @@ -145,6 +146,7 @@ // Queues json_queue request_input_queue; + json_queue > request_url_queue; json_queue confirm_shutdown_queue; json_queue prompt_new_edit_file_queue; json_queue message_dialog_queue; diff -r 4a1afb661c55 -r 7327936fa23e libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Fri Mar 18 09:47:13 2016 -0500 +++ b/libinterp/corefcn/octave-link.h Sat Dec 10 23:19:25 2016 +0000 @@ -289,6 +289,13 @@ : std::string (); } + static std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->do_request_url (url, param, action, success) + : std::string (); + } + static void enter_debugger_event (const std::string& file, int line) { if (enabled ()) @@ -439,6 +446,7 @@ bool _request_input_enabled; virtual std::string do_request_input (const std::string&) = 0; + virtual std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; virtual bool do_confirm_shutdown (void) = 0; virtual bool do_exit (int status) = 0; diff -r 4a1afb661c55 -r 7327936fa23e liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Fri Mar 18 09:47:13 2016 -0500 +++ b/liboctave/util/url-transfer.cc Sat Dec 10 23:19:25 2016 +0000 @@ -36,6 +36,7 @@ #include "dir-ops.h" #include "file-ops.h" #include "file-stat.h" +#include "../../libinterp/corefcn/octave-link.h" #include "unwind-prot.h" #include "url-transfer.h" @@ -214,6 +215,75 @@ return file_list; } + +class oo_url_transfer : public base_url_transfer +{ +public: + + oo_url_transfer (void) + : base_url_transfer () { + valid = true; + std::cout << "init method 1" << std::endl; + } + + oo_url_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + valid = true; + // url = "ftp://" + host; + std::cout << "init method 2" << std::endl; + // Set up the link, with no transfer. + // perform (); + } + + oo_url_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + valid = true; + std::cout << "init method 3" << std::endl; + } + + ~oo_url_transfer (void) {} + + void http_get (const Array& param) { + perform (param, "get"); + } + + void http_post (const Array& param) { + perform (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform (param, action); + } + +private: + void perform(const Array& param, const std::string& action) { + std::string url = host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (octave_link::request_input_enabled ()) { + bool success; + std::string result = octave_link::request_url (url, paramList, action, success); + if (success) { + (*curr_ostream) << result; + } else { + ok = false; + errmsg = result; + } + } else { + ok = false; + errmsg = "octave_link not connected for oo_url_transfer"; + } + } +}; + + #if defined (HAVE_CURL) static int @@ -769,11 +839,12 @@ #endif -#if defined (HAVE_CURL) -# define REP_CLASS curl_transfer -#else -# define REP_CLASS base_url_transfer -#endif +// #if defined (HAVE_CURL) +// # define REP_CLASS curl_transfer +// #else +// # define REP_CLASS base_url_transfer +// #endif +#define REP_CLASS oo_url_transfer url_transfer::url_transfer (void) : rep (new REP_CLASS ()) { } ================================================ FILE: back-octave/oo-changesets/012-84390db50239.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1481412676 28800 # Sat Dec 10 15:31:16 2016 -0800 # Branch oo # Node ID 84390db5023918d68c1c133642f9fa6cb6aa189d # Parent 7327936fa23e3070c748c8db6ef4d1a0fc473da1 Removing the "fork" command from the Octave interpreter. diff -r 7327936fa23e -r 84390db50239 libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Sat Dec 10 23:19:25 2016 +0000 +++ b/libinterp/corefcn/syscalls.cc Sat Dec 10 15:31:16 2016 -0800 @@ -167,9 +167,8 @@ @deftypefn {Built-in Function} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args})\n\ Replace current process with a new process.\n\ \n\ -Calling @code{exec} without first calling @code{fork} will terminate your\n\ -current Octave process and replace it with the program named by @var{file}.\n\ -For example,\n\ +Calling @code{exec} will terminate your current Octave process and replace\n\ +it with the program named by @var{file}. For example,\n\ \n\ @example\n\ exec (\"ls\" \"-l\")\n\ @@ -552,51 +551,6 @@ return retval; } -DEFUNX ("fork", Ffork, args, , - "-*- texinfo -*-\n\ -@deftypefn {Built-in Function} {[@var{pid}, @var{msg}] =} fork ()\n\ -Create a copy of the current process.\n\ -\n\ -Fork can return one of the following values:\n\ -\n\ -@table @asis\n\ -@item > 0\n\ -You are in the parent process. The value returned from @code{fork} is the\n\ -process id of the child process. You should probably arrange to wait for\n\ -any child processes to exit.\n\ -\n\ -@item 0\n\ -You are in the child process. You can call @code{exec} to start another\n\ -process. If that fails, you should probably call @code{exit}.\n\ -\n\ -@item < 0\n\ -The call to @code{fork} failed for some reason. You must take evasive\n\ -action. A system dependent error message will be waiting in @var{msg}.\n\ -@end table\n\ -@end deftypefn") -{ - octave_value_list retval; - - retval(1) = std::string (); - retval(0) = -1; - - int nargin = args.length (); - - if (nargin == 0) - { - std::string msg; - - pid_t pid = octave_syscalls::fork (msg); - - retval(1) = msg; - retval(0) = pid; - } - else - print_usage (); - - return retval; -} - DEFUNX ("getpgrp", Fgetpgrp, args, , "-*- texinfo -*-\n\ @deftypefn {Built-in Function} {pgid =} getpgrp ()\n\ ================================================ FILE: back-octave/oo-changesets/013-f4110d638cdb.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1481417641 28800 # Sat Dec 10 16:54:01 2016 -0800 # Branch oo # Node ID f4110d638cdb9b0a06d504cfe2420aad4e954a17 # Parent 84390db5023918d68c1c133642f9fa6cb6aa189d Removing debugging print statements from url-transfer.cc. diff -r 84390db50239 -r f4110d638cdb liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sat Dec 10 15:31:16 2016 -0800 +++ b/liboctave/util/url-transfer.cc Sat Dec 10 16:54:01 2016 -0800 @@ -223,7 +223,6 @@ oo_url_transfer (void) : base_url_transfer () { valid = true; - std::cout << "init method 1" << std::endl; } oo_url_transfer (const std::string& host, const std::string& user_arg, @@ -231,15 +230,11 @@ : base_url_transfer (host, user_arg, passwd, os) { valid = true; // url = "ftp://" + host; - std::cout << "init method 2" << std::endl; - // Set up the link, with no transfer. - // perform (); } oo_url_transfer (const std::string& url_str, std::ostream& os) : base_url_transfer (url_str, os) { valid = true; - std::cout << "init method 3" << std::endl; } ~oo_url_transfer (void) {} ================================================ FILE: back-octave/oo-changesets/014-21fd506b7530.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1481421015 28800 # Sat Dec 10 17:50:15 2016 -0800 # Branch oo # Node ID 21fd506b753001b4d568e34b06a9f8dd94c1d8df # Parent f4110d638cdb9b0a06d504cfe2420aad4e954a17 Adding support for binary text via base64 to oo_url_transfer. diff -r f4110d638cdb -r 21fd506b7530 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sat Dec 10 16:54:01 2016 -0800 +++ b/liboctave/util/url-transfer.cc Sat Dec 10 17:50:15 2016 -0800 @@ -37,6 +37,7 @@ #include "file-ops.h" #include "file-stat.h" #include "../../libinterp/corefcn/octave-link.h" +#include "base64.h" #include "unwind-prot.h" #include "url-transfer.h" @@ -266,7 +267,7 @@ bool success; std::string result = octave_link::request_url (url, paramList, action, success); if (success) { - (*curr_ostream) << result; + process_success(result); } else { ok = false; errmsg = result; @@ -276,6 +277,22 @@ errmsg = "octave_link not connected for oo_url_transfer"; } } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + ok = false; + errmsg = "failed decoding base64 from octave_link"; + } else { + curr_ostream->write(out, outlen); + ::free(out); + } + } }; ================================================ FILE: back-octave/oo-changesets/100-2d1fd5fdd1d5.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1514199455 21600 # Mon Dec 25 04:57:35 2017 -0600 # Branch oo-4.2.1 # Node ID 2d1fd5fdd1d591cb643542703e3679da48056fee # Parent b9d482dd90f32c78d544125ec063dbd88c6da476 # Parent 21fd506b753001b4d568e34b06a9f8dd94c1d8df Merging Octave Online patch into version 4.2.1. diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 configure.ac --- a/configure.ac Fri Dec 22 15:29:45 2017 -0800 +++ b/configure.ac Mon Dec 25 04:57:35 2017 -0600 @@ -2969,7 +2969,7 @@ LIBOCTINTERP_LINK_DEPS="$DLDFCN_LIBS" fi -LIBOCTINTERP_LINK_DEPS="$LIBOCTINTERP_LINK_DEPS $FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$LIBOCTINTERP_LINK_DEPS $FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $FFTW_XLDFLAGS $LLVM_LDFLAGS" diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libgui/src/octave-qt-link.cc --- a/libgui/src/octave-qt-link.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libgui/src/octave-qt-link.cc Mon Dec 25 04:57:35 2017 -0600 @@ -86,6 +86,16 @@ emit execute_interpreter_signal (); } +std::string octave_qt_link::do_request_input (const std::string&) +{ + return {}; +} + +std::string octave_qt_link::do_request_url (const std::string&, const std::list&, const std::string&, bool&) +{ + return {}; +} + bool octave_qt_link::do_confirm_shutdown (void) { @@ -530,6 +540,11 @@ } void +octave_qt_link::do_clear_screen (void) +{ +} + +void octave_qt_link::do_pre_input_event (void) { } @@ -673,6 +688,12 @@ } void +octave_qt_link::do_show_static_plot (const std::string&, const std::string&) +{ + return; +} + +void octave_qt_link::terminal_interrupt (void) { command_interpreter->interrupt (); diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libgui/src/octave-qt-link.h --- a/libgui/src/octave-qt-link.h Fri Dec 22 15:29:45 2017 -0800 +++ b/libgui/src/octave-qt-link.h Mon Dec 25 04:57:35 2017 -0600 @@ -62,6 +62,9 @@ void execute_interpreter (void); + std::string do_request_input (const std::string&); + std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool do_confirm_shutdown (void); bool do_exit (int status); @@ -118,6 +121,8 @@ void do_append_history (const std::string& hist_entry); void do_clear_history (void); + void do_clear_screen (void); + void do_pre_input_event (void); void do_post_input_event (void); @@ -137,6 +142,9 @@ void do_show_doc (const std::string& file); + void do_show_static_plot (const std::string& term, + const std::string& content); + QMutex mutex; QWaitCondition waitcondition; void shutdown_confirmation (bool sd) {_shutdown_confirm_result = sd;} diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/input.cc Mon Dec 25 04:57:35 2017 -0600 @@ -183,7 +183,11 @@ eof = false; - std::string retval = octave::command_editor::readline (s, eof); + std::string retval; + if (octave_link::request_input_enabled ()) + retval = octave_link::request_input (s); + else + retval = octave::command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/interpreter.cc Mon Dec 25 04:57:35 2017 -0600 @@ -47,6 +47,7 @@ #include "file-io.h" #include "graphics.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave-link.h" @@ -635,6 +636,11 @@ initialize_version_info (); + if (!options.json_sock_path().empty ()) { + static json_main _json_main (options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } + // Make all command-line arguments available to startup files, // including PKG_ADD files. diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Mon Dec 25 04:57:35 2017 -0600 @@ -0,0 +1,93 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (this) +{ + // Enable octave_json_link instance + octave_link::connect_link(&_octave_json_link); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link.receive_message(name, jobj); +} diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Mon Dec 25 04:57:35 2017 -0600 @@ -0,0 +1,30 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +class json_main { +public: + json_main(const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + octave_json_link _octave_json_link; +}; + +#endif diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Mon Dec 25 04:57:35 2017 -0600 @@ -0,0 +1,242 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + return json_object_new_string(str.c_str()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + typename std::list::const_iterator it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_workspace_list(const std::list& list) { + return json_object_from_list(list, json_util::from_workspace_element); +} + +JSON_OBJECT_T json_util::from_filter_list(const octave_link::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_workspace_element(workspace_element element) { + JSON_MAP_T m; + m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.symbol()); + m["class_name"] = json_util::from_string(element.class_name()); + m["dimension"] = json_util::from_string(element.dimension()); + m["value"] = json_util::from_string(element.value()); + m["complex_flag"] = json_util::from_boolean(element.complex_flag()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_object_new_string(pair.first.c_str())); + json_object_array_add(jobj, json_object_new_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (int i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Mon Dec 25 04:57:35 2017 -0600 @@ -0,0 +1,60 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "workspace-element.h" +#include "octave-link.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_workspace_list(const std::list& list); + static JSON_OBJECT_T from_filter_list(const octave_link::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_workspace_element(workspace_element element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +#endif diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/module.mk Mon Dec 25 04:57:35 2017 -0600 @@ -48,6 +48,8 @@ libinterp/corefcn/help.h \ libinterp/corefcn/hook-fcn.h \ libinterp/corefcn/input.h \ + libinterp/corefcn/json-main.h \ + libinterp/corefcn/json-util.h \ libinterp/corefcn/interpreter.h \ libinterp/corefcn/load-path.h \ libinterp/corefcn/load-save.h \ @@ -77,6 +79,7 @@ libinterp/corefcn/oct-strstrm.h \ libinterp/corefcn/oct.h \ libinterp/corefcn/octave-default-image.h \ + libinterp/corefcn/octave-json-link.h \ libinterp/corefcn/octave-link.h \ libinterp/corefcn/octave-preserve-stream-state.h \ libinterp/corefcn/pager.h \ @@ -179,6 +182,8 @@ libinterp/corefcn/hex2num.cc \ libinterp/corefcn/hook-fcn.cc \ libinterp/corefcn/input.cc \ + libinterp/corefcn/json-main.cc \ + libinterp/corefcn/json-util.cc \ libinterp/corefcn/inv.cc \ libinterp/corefcn/interpreter.cc \ libinterp/corefcn/kron.cc \ @@ -214,6 +219,7 @@ libinterp/corefcn/oct-tex-lexer.ll \ libinterp/corefcn/oct-tex-parser.h \ libinterp/corefcn/oct-tex-parser.yy \ + libinterp/corefcn/octave-json-link.cc \ libinterp/corefcn/octave-link.cc \ libinterp/corefcn/ordschur.cc \ libinterp/corefcn/pager.cc \ diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Mon Dec 25 04:57:35 2017 -0600 @@ -0,0 +1,362 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "workspace-element.h" +#include "json-main.h" +#include "json-util.h" + +octave_json_link::octave_json_link(json_main* __json_main) + : octave_link (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::do_request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::do_request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::do_confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +bool octave_json_link::do_exit(int status) { + JSON_MAP_T m; + JSON_MAP_SET(m, status, int); + _publish_message("exit", json_util::from_map(m)); + + // It is our responsibility in octave_link to call exit. If we don't, then + // the kernel waits for 24 hours expecting us to do something. + ::exit(status); + + return true; +} + +bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { + // Triggered in "msgbox", "helpdlg", and "errordlg", among others + JSON_MAP_T m; + JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + _publish_message("message-dialog", json_util::from_map(m)); + + return message_dialog_queue.dequeue(); +} + +std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::do_list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::do_debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::do_change_directory(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::do_execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +void octave_json_link::do_set_workspace(bool top_level, bool debug, const std::list& ws /*, const bool& variable_editor_too */) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, workspace_list); + // variable_editor_too? + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::do_clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::do_set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::do_append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::do_clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::do_clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::do_pre_input_event(void) { + // noop +} + +void octave_json_link::do_post_input_event(void) { + // noop +} + +void octave_json_link::do_enter_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::do_update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { + // Triggered upon interpreter startup + JSON_MAP_T m; + JSON_MAP_SET(m, ps1, string); + JSON_MAP_SET(m, ps2, string); + JSON_MAP_SET(m, ps4, string); + _publish_message("set-default-prompts", json_util::from_map(m)); +} + +void octave_json_link::do_show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +void octave_json_link::do_show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +// void octave_json_link::do_openvar(const std::string& name) { +// // Triggered on "openvar" command +// _publish_message("openvar", json_util::from_string(name)); +// } + +void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Mon Dec 25 04:57:35 2017 -0600 @@ -0,0 +1,209 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "octave-link.h" +#include "json-util.h" +#include "oct-mutex.h" + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + octave_mutex _mutex; +}; + +class octave_json_link : public octave_link +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string do_request_input (const std::string& prompt) override; + std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + bool do_confirm_shutdown (void) override; + bool do_exit (int status) override; + + bool do_copy_image_to_clipboard (const std::string& file) override; + + bool do_edit_file (const std::string& file) override; + bool do_prompt_new_edit_file (const std::string& file) override; + + int do_message_dialog (const std::string& dlg, const std::string& msg, + const std::string& title) override; + + std::string + do_question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + do_list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + do_input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + do_file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + int + do_debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + void do_change_directory (const std::string& dir) override; + + void do_execute_command_in_terminal (const std::string& command) override; + + void do_set_workspace (bool top_level, bool debug, + const std::list& ws + // Added on head but not yet in stable: + // , const bool& variable_editor_too = true + ) override; + + void do_clear_workspace (void) override; + + void do_set_history (const string_vector& hist) override; + void do_append_history (const std::string& hist_entry) override; + void do_clear_history (void) override; + + void do_clear_screen (void) override; + + void do_pre_input_event (void) override; + void do_post_input_event (void) override; + + void do_enter_debugger_event (const std::string& file, int line) override; + void do_execute_in_debugger_event (const std::string& file, int line) override; + void do_exit_debugger_event (void) override; + + void do_update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + void do_set_default_prompts (std::string& ps1, std::string& ps2, + std::string& ps4) override; + + void do_show_preferences (void) override; + + void do_show_doc (const std::string& file) override; + + // Added on head but not yet in stable: + // void do_openvar (const std::string& name) override; + + void do_show_static_plot (const std::string& term, + const std::string& content) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +#endif diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/octave-link.cc --- a/libinterp/corefcn/octave-link.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/octave-link.cc Mon Dec 25 04:57:35 2017 -0600 @@ -395,3 +395,30 @@ return ovl (octave_link::show_doc (file)); } + +DEFUN (__octave_link_plot_destination__, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __octave_link_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (octave_link::plot_destination ()); +} + +DEFUN (__octave_link_show_static_plot__, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __octave_link_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (octave_link::show_static_plot (term, content)); +} + + + diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/octave-link.h Mon Dec 25 04:57:35 2017 -0600 @@ -265,6 +265,12 @@ instance->do_clear_history (); } + static void clear_screen (void) + { + if (enabled ()) + instance->do_clear_screen (); + } + static void pre_input_event (void) { if (enabled ()) @@ -277,6 +283,20 @@ instance->do_post_input_event (); } + static std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->do_request_input (prompt) + : std::string (); + } + + static std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->do_request_url (url, param, action, success) + : std::string (); + } + static void enter_debugger_event (const std::string& file, int line) { if (enabled ()) @@ -325,6 +345,34 @@ return instance_ok () ? instance->link_enabled : false; } + static bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + + static plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + static bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->do_show_static_plot (term, content); + return true; + } + else + return false; + } + static bool show_preferences () { @@ -398,6 +446,10 @@ void do_entered_readline_hook (void) { } void do_finished_readline_hook (void) { } + bool _request_input_enabled; + virtual std::string do_request_input (const std::string&) = 0; + virtual std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + virtual bool do_confirm_shutdown (void) = 0; virtual bool do_exit (int status) = 0; @@ -456,6 +508,8 @@ virtual void do_append_history (const std::string& hist_entry) = 0; virtual void do_clear_history (void) = 0; + virtual void do_clear_screen (void) = 0; + virtual void do_pre_input_event (void) = 0; virtual void do_post_input_event (void) = 0; @@ -476,7 +530,11 @@ virtual void do_show_preferences (void) = 0; - virtual void do_show_doc (const std::string &file) = 0; + virtual void do_show_doc (const std::string& file) = 0; + + plot_destination_t _plot_destination; + virtual void do_show_static_plot (const std::string& term, + const std::string& content) = 0; }; #endif diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/syscalls.cc Mon Dec 25 04:57:35 2017 -0600 @@ -143,9 +143,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -453,41 +452,6 @@ return ovl (status, msg); } -DEFUNX ("fork", Ffork, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (symbol_table::at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = octave::sys::fork (msg); - - return ovl (pid, msg); -} DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/corefcn/sysdep.cc Mon Dec 25 04:57:35 2017 -0600 @@ -79,6 +79,7 @@ #include "errwarn.h" #include "input.h" #include "octave.h" +#include "octave-link.h" #include "ov.h" #include "ovl.h" #include "pager.h" @@ -624,6 +625,8 @@ { bool skip_redisplay = true; + octave_link::clear_screen (); + octave::command_editor::clear_screen (skip_redisplay); return ovl (); diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/octave.cc --- a/libinterp/octave.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/octave.cc Mon Dec 25 04:57:35 2017 -0600 @@ -45,6 +45,7 @@ #include "octave.h" #include "oct-hist.h" #include "oct-map.h" +#include "octave-link.h" #include "ovl.h" #include "options-usage.h" #include "ov.h" @@ -188,6 +189,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_no_gui = true; @@ -264,6 +275,8 @@ m_command_line_path (opts.m_command_line_path), m_exec_path (opts.m_exec_path), m_image_path (opts.m_image_path), + m_json_sock_path (opts.m_json_sock_path), + m_json_max_message_length (opts.m_json_max_message_length), m_all_args (opts.m_all_args), m_remaining_args (opts.m_remaining_args) { } @@ -291,6 +304,8 @@ m_command_line_path = opts.m_command_line_path; m_exec_path = opts.m_exec_path; m_image_path = opts.m_image_path; + m_json_sock_path = opts.m_json_sock_path; + m_json_max_message_length = opts.m_json_max_message_length; m_all_args = opts.m_all_args; m_remaining_args = opts.m_remaining_args; } @@ -472,7 +487,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (octave::application::is_gui_running ()); + return ovl (octave_link::enabled ()); } /* diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/octave.h --- a/libinterp/octave.h Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/octave.h Mon Dec 25 04:57:35 2017 -0600 @@ -66,6 +66,8 @@ std::list command_line_path (void) const { return m_command_line_path; } std::string exec_path (void) const { return m_exec_path; } std::string image_path (void) const { return m_image_path; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -87,6 +89,8 @@ void command_line_path (const std::list& arg) { m_command_line_path = arg; } void exec_path (const std::string& arg) { m_exec_path = arg; } void image_path (const std::string& arg) { m_image_path = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -164,6 +168,14 @@ // (--image-path) std::string m_image_path; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 libinterp/options-usage.h --- a/libinterp/options-usage.h Fri Dec 22 15:29:45 2017 -0800 +++ b/libinterp/options-usage.h Mon Dec 25 04:57:35 2017 -0600 @@ -35,10 +35,10 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--force-gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--line-editing] [--no-gui] [--no-history]\n\ - [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--silent] [--traditional]\n\ + [--jit-compiler] [--json-sock] [--json-max-len] [--line-editing]\n\ + [--no-gui] [--no-history][--no-init-file] [--no-init-path]\n\ + [--no-line-editing] [--no-site-file] [--no-window-system] [--norc]\n\ + [-p path] [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; // This is here so that it's more likely that the usage message and @@ -58,15 +58,17 @@ #define INFO_PROG_OPTION 8 #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 -#define LINE_EDITING_OPTION 11 -#define NO_GUI_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define TEXI_MACROS_FILE_OPTION 18 -#define TRADITIONAL_OPTION 19 +#define JSON_SOCK_OPTION 11 +#define JSON_MAX_LEN_OPTION 12 +#define LINE_EDITING_OPTION 13 +#define NO_GUI_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define TEXI_MACROS_FILE_OPTION 20 +#define TRADITIONAL_OPTION 21 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, 0, TRADITIONAL_OPTION }, @@ -84,6 +86,8 @@ { "info-program", octave_required_arg, 0, INFO_PROG_OPTION }, { "interactive", octave_no_arg, 0, 'i' }, { "jit-compiler", octave_no_arg, 0, JIT_COMPILER_OPTION }, + { "json-sock", octave_required_arg, 0, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, 0, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, 0, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, 0, NO_GUI_OPTION }, { "no-history", octave_no_arg, 0, 'H' }, @@ -130,6 +134,8 @@ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/liboctave/util/oct-mutex.cc Mon Dec 25 04:57:35 2017 -0600 @@ -53,6 +53,18 @@ return false; } +void +octave_base_mutex::cond_wait (void) +{ + (*current_liboctave_error_handler) ("mutex not supported on this platform"); +} + +void +octave_base_mutex::cond_signal (void) +{ + (*current_liboctave_error_handler) ("mutex not supported on this platform"); +} + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -63,11 +75,13 @@ : octave_base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~octave_w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -85,8 +99,21 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + + void + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD octave_thread_id = 0; @@ -118,11 +145,18 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~octave_pthread_mutex (void) { pthread_mutex_destroy (&pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -140,8 +174,20 @@ return (pthread_mutex_trylock (&pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t pm; + pthread_cond_t condv; + }; static pthread_t octave_thread_id = 0; diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Fri Dec 22 15:29:45 2017 -0800 +++ b/liboctave/util/oct-mutex.h Mon Dec 25 04:57:35 2017 -0600 @@ -45,6 +45,10 @@ virtual bool try_lock (void); + virtual void cond_wait (void); + + virtual void cond_signal (void); + private: octave_refcount count; }; @@ -97,6 +101,16 @@ return rep->try_lock (); } + void cond_wait (void) + { + rep->cond_wait (); + } + + void cond_signal (void) + { + rep->cond_signal (); + } + protected: octave_base_mutex *rep; }; diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Fri Dec 22 15:29:45 2017 -0800 +++ b/liboctave/util/url-transfer.cc Mon Dec 25 04:57:35 2017 -0600 @@ -36,6 +36,8 @@ #include "dir-ops.h" #include "file-ops.h" #include "file-stat.h" +#include "libinterp/corefcn/octave-link.h" +#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" @@ -216,6 +218,86 @@ return file_list; } + +class oo_url_transfer : public base_url_transfer +{ +public: + + oo_url_transfer (void) + : base_url_transfer () { + valid = true; + } + + oo_url_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + valid = true; + // url = "ftp://" + host; + } + + oo_url_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + valid = true; + } + + ~oo_url_transfer (void) {} + + void http_get (const Array& param) { + perform (param, "get"); + } + + void http_post (const Array& param) { + perform (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform (param, action); + } + +private: + void perform(const Array& param, const std::string& action) { + std::string url = host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (octave_link::request_input_enabled ()) { + bool success; + std::string result = octave_link::request_url (url, paramList, action, success); + if (success) { + process_success(result); + } else { + ok = false; + errmsg = result; + } + } else { + ok = false; + errmsg = "octave_link not connected for oo_url_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + ok = false; + errmsg = "failed decoding base64 from octave_link"; + } else { + curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -771,11 +853,12 @@ #endif -#if defined (HAVE_CURL) -# define REP_CLASS curl_transfer -#else -# define REP_CLASS base_url_transfer -#endif +// #if defined (HAVE_CURL) +// # define REP_CLASS curl_transfer +// #else +// # define REP_CLASS base_url_transfer +// #endif +#define REP_CLASS oo_url_transfer url_transfer::url_transfer (void) : rep (new REP_CLASS ()) { } diff -r b9d482dd90f3 -r 2d1fd5fdd1d5 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Fri Dec 22 15:29:45 2017 -0800 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Mon Dec 25 04:57:35 2017 -0600 @@ -27,9 +27,79 @@ if (nargin < 1 || nargin > 4 || nargin == 2) print_usage (); + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __octave_link_plot_destination__ () == 0 || + __octave_link_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced, mono); + fflush (plot_stream(1)); + endif + + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __octave_link_plot_destination__ () == 1 || + __octave_link_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + pause (1); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __octave_link_show_static_plot__ (term, a); + endif + endif + endif + endif +endfunction - if (nargin >= 3 && nargin <= 4) +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -65,44 +135,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/100-README.md ================================================ Patch file `100` contains all previous patch files combined and applied against commit `b9d482dd90f3` in the GNU Octave mercurial repository, which is in the stable branch of version *4.2*. Patch files `101` and up are additional commits. ================================================ FILE: back-octave/oo-changesets/101-bc8cd93feec5.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1514203589 21600 # Mon Dec 25 06:06:29 2017 -0600 # Branch oo-4.2.1 # Node ID bc8cd93feec50aa5f30d6ad0aa0e85f31693c71f # Parent 2d1fd5fdd1d591cb643542703e3679da48056fee Removing pause(1) from the mandatory critical path for plotting. diff -r 2d1fd5fdd1d5 -r bc8cd93feec5 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Mon Dec 25 04:57:35 2017 -0600 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Mon Dec 25 06:06:29 2017 -0600 @@ -62,7 +62,7 @@ if (should_plot_to_terminal) enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced, mono); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); fflush (plot_stream(1)); endif @@ -75,10 +75,15 @@ if (should_plot_to_temp_file) tmp_file = tempname (); __gnuplot_draw_to_file__ (h, term, tmp_file); - pause (1); + fflush (plot_stream(1)); ## Read the temp file into memory and then delete it fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile [a, count] = fscanf (fid, '%c', Inf); fclose (fid); unlink (tmp_file); ================================================ FILE: back-octave/oo-changesets/102-30d8ba0fbc32.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1514234238 21600 # Mon Dec 25 14:37:18 2017 -0600 # Branch oo-4.2.1 # Node ID 30d8ba0fbc32805a870089c2159fc74cec21d22a # Parent bc8cd93feec50aa5f30d6ad0aa0e85f31693c71f In urlread, enable octave_link::request_url only when octave_link is enabled. diff -r bc8cd93feec5 -r 30d8ba0fbc32 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Mon Dec 25 06:06:29 2017 -0600 +++ b/liboctave/util/url-transfer.cc Mon Dec 25 14:37:18 2017 -0600 @@ -219,28 +219,28 @@ } -class oo_url_transfer : public base_url_transfer +class link_transfer : public base_url_transfer { public: - oo_url_transfer (void) + link_transfer (void) : base_url_transfer () { valid = true; } - oo_url_transfer (const std::string& host, const std::string& user_arg, + link_transfer (const std::string& host, const std::string& user_arg, const std::string& passwd, std::ostream& os) : base_url_transfer (host, user_arg, passwd, os) { valid = true; // url = "ftp://" + host; } - oo_url_transfer (const std::string& url_str, std::ostream& os) + link_transfer (const std::string& url_str, std::ostream& os) : base_url_transfer (url_str, os) { valid = true; } - ~oo_url_transfer (void) {} + ~link_transfer (void) {} void http_get (const Array& param) { perform (param, "get"); @@ -276,7 +276,7 @@ } } else { ok = false; - errmsg = "octave_link not connected for oo_url_transfer"; + errmsg = "octave_link not connected for link_transfer"; } } @@ -853,24 +853,36 @@ #endif -// #if defined (HAVE_CURL) -// # define REP_CLASS curl_transfer -// #else -// # define REP_CLASS base_url_transfer -// #endif -#define REP_CLASS oo_url_transfer +#if defined (HAVE_CURL) +# define REP_CLASS curl_transfer +#else +# define REP_CLASS base_url_transfer +#endif - url_transfer::url_transfer (void) : rep (new REP_CLASS ()) - { } + url_transfer::url_transfer (void) { + if (octave_link::request_input_enabled()) { + rep = new link_transfer(); + } else { + rep = new REP_CLASS(); + } + } url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : rep (new REP_CLASS (host, user, passwd, os)) - { } + const std::string& passwd, std::ostream& os) { + if (octave_link::request_input_enabled()) { + rep = new link_transfer(host, user, passwd, os); + } else { + rep = new REP_CLASS(host, user, passwd, os); + } + } - url_transfer::url_transfer (const std::string& url, std::ostream& os) - : rep (new REP_CLASS (url, os)) - { } + url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (octave_link::request_input_enabled()) { + rep = new link_transfer(url, os); + } else { + rep = new REP_CLASS(url, os); + } + } #undef REP_CLASS ================================================ FILE: back-octave/oo-changesets/103-352b599bc533.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1514767734 21600 # Sun Dec 31 18:48:54 2017 -0600 # Branch oo-4.2.1 # Node ID 352b599bc5337bc9c3465092aa36e320811f781a # Parent 30d8ba0fbc32805a870089c2159fc74cec21d22a Changing octave_kbhit() to use octave_link::request_input, fixing the functions kbhit and pause. diff -r 30d8ba0fbc32 -r 352b599bc533 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Mon Dec 25 14:37:18 2017 -0600 +++ b/libinterp/corefcn/sysdep.cc Sun Dec 31 18:48:54 2017 -0600 @@ -539,8 +539,8 @@ // Read one character from the terminal. -int -octave_kbhit (bool wait) +static int +octave_kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -563,10 +563,19 @@ octave::set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); - - if (std::cin.fail () || std::cin.eof ()) - std::cin.clear (); + int c; + if (octave_link::request_input_enabled ()) { + std::string line = octave_link::request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); + if (std::cin.fail () || std::cin.eof ()) + std::cin.clear (); + } // Restore it, enabling system call restarts (if possible). octave::set_interrupt_handler (saved_interrupt_handler, true); @@ -577,6 +586,12 @@ return c; } +int +octave_kbhit (bool wait) +{ + return octave_kbhit("", wait); +} + std::string get_P_tmpdir (void) { @@ -760,7 +775,7 @@ { Fdrawnow (); - int c = octave_kbhit (args.length () == 0); + int c = octave_kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; @@ -824,7 +839,7 @@ if (octave::math::isinf (dval)) { flush_octave_stdout (); - octave_kbhit (); + octave_kbhit ("press enter to continue", false); } else octave_sleep (dval); @@ -834,7 +849,7 @@ { Fdrawnow (); flush_octave_stdout (); - octave_kbhit (); + octave_kbhit ("press enter to continue", false); } return ovl (); ================================================ FILE: back-octave/oo-changesets/104-9475120a3110.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1515058277 21600 # Thu Jan 04 03:31:17 2018 -0600 # Branch oo-4.2.1 # Node ID 9475120a3110c6932c85fd16ea40dc51137ce78e # Parent 352b599bc5337bc9c3465092aa36e320811f781a Checking unknown functions against all available functions in installed packages. diff -r 352b599bc533 -r 9475120a3110 scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Sun Dec 31 18:48:54 2017 -0600 +++ b/scripts/help/__unimplemented__.m Thu Jan 04 03:31:17 2018 -0600 @@ -40,7 +40,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -511,6 +534,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -553,13 +577,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run `pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction ================================================ FILE: back-octave/oo-changesets/105-ccbef5c9b050.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1515058304 21600 # Thu Jan 04 03:31:44 2018 -0600 # Branch oo-4.2.1 # Node ID ccbef5c9b050b55218de6bdfb7d241fc90e0d032 # Parent 9475120a3110c6932c85fd16ea40dc51137ce78e Adding Octave binding for current_command_number variable. diff -r 9475120a3110 -r ccbef5c9b050 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Thu Jan 04 03:31:17 2018 -0600 +++ b/libinterp/corefcn/input.cc Thu Jan 04 03:31:44 2018 -0600 @@ -1591,3 +1591,32 @@ return retval; } + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} ================================================ FILE: back-octave/oo-changesets/106-91cb270ffac0.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1515059962 21600 # Thu Jan 04 03:59:22 2018 -0600 # Branch oo-4.2.1 # Node ID 91cb270ffac07018236b9e394378051df7b612c5 # Parent ccbef5c9b050b55218de6bdfb7d241fc90e0d032 Changing package_metadata.mat load to use absolute path. diff -r ccbef5c9b050 -r 91cb270ffac0 scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Thu Jan 04 03:31:44 2018 -0600 +++ b/scripts/help/__unimplemented__.m Thu Jan 04 03:59:22 2018 -0600 @@ -41,10 +41,10 @@ is_matlab_function = true; ## First look at the package metadata - # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('package_metadata.mat', 'packages'); + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); found_in_package_metadata = false; try - vars = load("package_metadata.mat"); + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); for lvl1 = vars.packages for lvl2 = lvl1{1}.provides for lvl3 = lvl2{1}.functions ================================================ FILE: back-octave/oo-changesets/107-80081f9d8ff7.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1515065941 21600 # Thu Jan 04 05:39:01 2018 -0600 # Branch oo-4.2.1 # Node ID 80081f9d8ff7179f1d5d82293f4c83030b8e0357 # Parent 91cb270ffac07018236b9e394378051df7b612c5 Sending current command number along with show-static-plot octave_link message. diff -r 91cb270ffac0 -r 80081f9d8ff7 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Thu Jan 04 03:59:22 2018 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Thu Jan 04 05:39:01 2018 -0600 @@ -27,6 +27,7 @@ #include #include "octave-json-link.h" #include "workspace-element.h" +#include "cmd-edit.h" #include "json-main.h" #include "json-util.h" @@ -304,9 +305,11 @@ void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = octave::command_editor::current_command_number(); JSON_MAP_T m; JSON_MAP_SET(m, term, string); JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); _publish_message("show-static-plot", json_util::from_map(m)); } ================================================ FILE: back-octave/oo-changesets/108-9b39ca8bcbfd.hg.txt ================================================ # HG changeset patch # User Octave Online # Date 1555656254 0 # Fri Apr 19 06:44:14 2019 +0000 # Branch stable # Node ID 9b39ca8bcbfd5398e342a6d8f9f81ac06069307b # Parent abdfdd6f14cc4063475c5968c4623b3ebd0cddde Minor gnuplot plotting fixes. diff -r abdfdd6f14cc -r 9b39ca8bcbfd scripts/plot/appearance/legend.m --- a/scripts/plot/appearance/legend.m Thu Jan 04 05:39:01 2018 -0600 +++ b/scripts/plot/appearance/legend.m Fri Apr 19 06:44:14 2019 +0000 @@ -563,8 +563,8 @@ old_hplots = [ get(hlegend, "deletefcn"){6:end} ]; endif if (addprops) - addproperty ("edgecolor", hlegend, "color", [0, 0, 0]); - addproperty ("textcolor", hlegend, "color", [0, 0, 0]); + addproperty ("edgecolor", hlegend, "color", get(0, "defaultaxesxcolor")); + addproperty ("textcolor", hlegend, "color", get(0, "defaulttextcolor")); locations = {"north", "south", "east", "west", ... "{northeast}", "southeast", "northwest", "southwest", ... "northoutside", "southoutside", ... diff -r abdfdd6f14cc -r 9b39ca8bcbfd scripts/plot/util/private/__gnuplot_draw_axes__.m --- a/scripts/plot/util/private/__gnuplot_draw_axes__.m Thu Jan 04 05:39:01 2018 -0600 +++ b/scripts/plot/util/private/__gnuplot_draw_axes__.m Fri Apr 19 06:44:14 2019 +0000 @@ -2880,6 +2880,8 @@ else cdata(:) = fix (255 / 2); endif + ## OO PATCH + cdata = 1 + cdata * (cmap_sz-1)/255; else if (islogical (cdata)) cdata += 1; ================================================ FILE: back-octave/oo-changesets/200-84cbf166497f.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1577968674 21600 # Thu Jan 02 06:37:54 2020 -0600 # Branch oo-5.2 # Node ID 84cbf166497fa3d942be6a0b026ec24521301d16 # Parent 56dd7419d7aa197340040f914d8ea69aa948710e # Parent 80081f9d8ff7179f1d5d82293f4c83030b8e0357 Merge oo-4.2.1 onto stable (5.2-rc) diff -r 56dd7419d7aa -r 84cbf166497f configure.ac --- a/configure.ac Tue Dec 24 04:01:33 2019 +0100 +++ b/configure.ac Thu Jan 02 06:37:54 2020 -0600 @@ -2752,7 +2752,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $FFTW_XLDFLAGS $LLVM_LDFLAGS" diff -r 56dd7419d7aa -r 84cbf166497f libgui/src/octave-qt-link.cc --- a/libgui/src/octave-qt-link.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libgui/src/octave-qt-link.cc Thu Jan 02 06:37:54 2020 -0600 @@ -62,6 +62,16 @@ qRegisterMetaType ("symbol_info_list"); } + std::string octave_qt_link::do_request_input (const std::string&) + { + return {}; + } + + std::string octave_qt_link::do_request_url (const std::string&, const std::list&, const std::string&, bool&) + { + return {}; + } + bool octave_qt_link::do_confirm_shutdown (void) { // Lock the mutex before emitting signal. @@ -470,6 +480,9 @@ emit clear_history_signal (); } + void octave_qt_link::do_clear_screen (void) + { } + void octave_qt_link::do_pre_input_event (void) { } @@ -645,4 +658,10 @@ { emit delete_debugger_pointer_signal (QString::fromStdString (file), line); } + + void + octave_qt_link::do_show_static_plot (const std::string&, const std::string&) + { + return; + } } diff -r 56dd7419d7aa -r 84cbf166497f libgui/src/octave-qt-link.h --- a/libgui/src/octave-qt-link.h Tue Dec 24 04:01:33 2019 +0100 +++ b/libgui/src/octave-qt-link.h Thu Jan 02 06:37:54 2020 -0600 @@ -67,6 +67,9 @@ ~octave_qt_link (void) = default; + std::string do_request_input (const std::string&); + std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool do_confirm_shutdown (void); bool do_copy_image_to_clipboard (const std::string& file); @@ -126,6 +129,8 @@ void do_append_history (const std::string& hist_entry); void do_clear_history (void); + void do_clear_screen (void); + void do_pre_input_event (void); void do_post_input_event (void); @@ -148,6 +153,8 @@ void do_edit_variable (const std::string& name, const octave_value& val); + void do_show_static_plot (const std::string& term, const std::string& content); + void shutdown_confirmation (bool sd) { m_shutdown_confirm_result = sd; } void lock (void) { m_mutex.lock (); } diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/input.cc Thu Jan 02 06:37:54 2020 -0600 @@ -740,7 +740,11 @@ eof = false; - std::string retval = octave::command_editor::readline (s, eof); + std::string retval; + if (octave_link::request_input_enabled ()) + retval = octave_link::request_input (s); + else + retval = octave::command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1674,3 +1678,32 @@ } // #endif + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/interpreter.cc Thu Jan 02 06:37:54 2020 -0600 @@ -51,6 +51,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave-link.h" @@ -518,6 +519,11 @@ initialize_version_info (); + if (!options.json_sock_path().empty ()) { + static json_main _json_main (options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } + // This should be done before initializing the load path because // some PKG_ADD files might need --traditional behavior. diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Thu Jan 02 06:37:54 2020 -0600 @@ -0,0 +1,93 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (this) +{ + // Enable octave_json_link instance + octave_link::connect_link(&_octave_json_link); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link.receive_message(name, jobj); +} diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Thu Jan 02 06:37:54 2020 -0600 @@ -0,0 +1,30 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +class json_main { +public: + json_main(const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + octave_json_link _octave_json_link; +}; + +#endif diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Thu Jan 02 06:37:54 2020 -0600 @@ -0,0 +1,242 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + return json_object_new_string(str.c_str()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + typename std::list::const_iterator it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_workspace_list(const std::list& list) { + return json_object_from_list(list, json_util::from_workspace_element); +} + +JSON_OBJECT_T json_util::from_filter_list(const octave_link::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_workspace_element(workspace_element element) { + JSON_MAP_T m; + m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.symbol()); + m["class_name"] = json_util::from_string(element.class_name()); + m["dimension"] = json_util::from_string(element.dimension()); + m["value"] = json_util::from_string(element.value()); + m["complex_flag"] = json_util::from_boolean(element.complex_flag()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_object_new_string(pair.first.c_str())); + json_object_array_add(jobj, json_object_new_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (int i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Thu Jan 02 06:37:54 2020 -0600 @@ -0,0 +1,60 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "workspace-element.h" +#include "octave-link.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_workspace_list(const std::list& list); + static JSON_OBJECT_T from_filter_list(const octave_link::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_workspace_element(workspace_element element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +#endif diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/module.mk Thu Jan 02 06:37:54 2020 -0600 @@ -45,6 +45,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/load-path.h \ %reldir%/load-save.h \ @@ -73,6 +75,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/octave-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ @@ -175,6 +178,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/inv.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ @@ -210,6 +215,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/octave-link.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Thu Jan 02 06:37:54 2020 -0600 @@ -0,0 +1,365 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "workspace-element.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +octave_json_link::octave_json_link(json_main* __json_main) + : octave_link (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::do_request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::do_request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::do_confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +bool octave_json_link::do_exit(int status) { + JSON_MAP_T m; + JSON_MAP_SET(m, status, int); + _publish_message("exit", json_util::from_map(m)); + + // It is our responsibility in octave_link to call exit. If we don't, then + // the kernel waits for 24 hours expecting us to do something. + ::exit(status); + + return true; +} + +bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { + // Triggered in "msgbox", "helpdlg", and "errordlg", among others + JSON_MAP_T m; + JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + _publish_message("message-dialog", json_util::from_map(m)); + + return message_dialog_queue.dequeue(); +} + +std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::do_list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::do_debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::do_change_directory(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::do_execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +void octave_json_link::do_set_workspace(bool top_level, bool debug, const std::list& ws /*, const bool& variable_editor_too */) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, workspace_list); + // variable_editor_too? + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::do_clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::do_set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::do_append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::do_clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::do_clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::do_pre_input_event(void) { + // noop +} + +void octave_json_link::do_post_input_event(void) { + // noop +} + +void octave_json_link::do_enter_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::do_update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { + // Triggered upon interpreter startup + JSON_MAP_T m; + JSON_MAP_SET(m, ps1, string); + JSON_MAP_SET(m, ps2, string); + JSON_MAP_SET(m, ps4, string); + _publish_message("set-default-prompts", json_util::from_map(m)); +} + +void octave_json_link::do_show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +void octave_json_link::do_show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +// void octave_json_link::do_openvar(const std::string& name) { +// // Triggered on "openvar" command +// _publish_message("openvar", json_util::from_string(name)); +// } + +void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = octave::command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Thu Jan 02 06:37:54 2020 -0600 @@ -0,0 +1,209 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "octave-link.h" +#include "json-util.h" +#include "oct-mutex.h" + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + octave_mutex _mutex; +}; + +class octave_json_link : public octave_link +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string do_request_input (const std::string& prompt) override; + std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + bool do_confirm_shutdown (void) override; + bool do_exit (int status) override; + + bool do_copy_image_to_clipboard (const std::string& file) override; + + bool do_edit_file (const std::string& file) override; + bool do_prompt_new_edit_file (const std::string& file) override; + + int do_message_dialog (const std::string& dlg, const std::string& msg, + const std::string& title) override; + + std::string + do_question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + do_list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + do_input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + do_file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + int + do_debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + void do_change_directory (const std::string& dir) override; + + void do_execute_command_in_terminal (const std::string& command) override; + + void do_set_workspace (bool top_level, bool debug, + const std::list& ws + // Added on head but not yet in stable: + // , const bool& variable_editor_too = true + ) override; + + void do_clear_workspace (void) override; + + void do_set_history (const string_vector& hist) override; + void do_append_history (const std::string& hist_entry) override; + void do_clear_history (void) override; + + void do_clear_screen (void) override; + + void do_pre_input_event (void) override; + void do_post_input_event (void) override; + + void do_enter_debugger_event (const std::string& file, int line) override; + void do_execute_in_debugger_event (const std::string& file, int line) override; + void do_exit_debugger_event (void) override; + + void do_update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + void do_set_default_prompts (std::string& ps1, std::string& ps2, + std::string& ps4) override; + + void do_show_preferences (void) override; + + void do_show_doc (const std::string& file) override; + + // Added on head but not yet in stable: + // void do_openvar (const std::string& name) override; + + void do_show_static_plot (const std::string& term, + const std::string& content) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +#endif diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/octave-link.cc --- a/libinterp/corefcn/octave-link.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/octave-link.cc Thu Jan 02 06:37:54 2020 -0600 @@ -525,3 +525,28 @@ return ovl (octave_link::unregister_doc (file)); } + +DEFUN (__octave_link_plot_destination__, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __octave_link_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (octave_link::plot_destination ()); +} + +DEFUN (__octave_link_show_static_plot__, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __octave_link_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (octave_link::show_static_plot (term, content)); +} + diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/octave-link.h Thu Jan 02 06:37:54 2020 -0600 @@ -278,6 +278,12 @@ instance->do_clear_history (); } + static void clear_screen (void) + { + if (enabled ()) + instance->do_clear_screen (); + } + static void pre_input_event (void) { if (enabled ()) @@ -290,6 +296,20 @@ instance->do_post_input_event (); } + static std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->do_request_input (prompt) + : std::string (); + } + + static std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->do_request_url (url, param, action, success) + : std::string (); + } + static void enter_debugger_event (const std::string& file, int line) { if (enabled ()) @@ -371,6 +391,34 @@ return instance_ok () ? instance->link_enabled : false; } + static bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + + static plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + static bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->do_show_static_plot (term, content); + return true; + } + else + return false; + } + static bool show_preferences () { @@ -486,6 +534,10 @@ void do_entered_readline_hook (void) { } void do_finished_readline_hook (void) { } + bool _request_input_enabled; + virtual std::string do_request_input (const std::string&) = 0; + virtual std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + virtual bool do_confirm_shutdown (void) = 0; virtual bool do_copy_image_to_clipboard (const std::string& file) = 0; @@ -546,6 +598,8 @@ virtual void do_append_history (const std::string& hist_entry) = 0; virtual void do_clear_history (void) = 0; + virtual void do_clear_screen (void) = 0; + virtual void do_pre_input_event (void) = 0; virtual void do_post_input_event (void) = 0; @@ -574,6 +628,10 @@ virtual void do_edit_variable (const std::string& name, const octave_value& val) = 0; + + plot_destination_t _plot_destination; + virtual void do_show_static_plot (const std::string& term, + const std::string& content) = 0; }; #endif diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/syscalls.cc Thu Jan 02 06:37:54 2020 -0600 @@ -146,9 +146,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -458,44 +457,6 @@ return ovl (status, msg); } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - octave::symbol_table& symtab = interp.get_symbol_table (); - - if (symtab.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = octave::sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/sysdep.cc Thu Jan 02 06:37:54 2020 -0600 @@ -74,6 +74,7 @@ #include "errwarn.h" #include "input.h" #include "octave.h" +#include "octave-link.h" #include "ov.h" #include "ovl.h" #include "pager.h" @@ -549,7 +550,7 @@ // Read one character from the terminal. - int kbhit (bool wait) + int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -576,13 +577,23 @@ octave::set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + if (octave_link::request_input_enabled ()) { + std::string line = octave_link::request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). octave::set_interrupt_handler (saved_interrupt_handler, true); @@ -641,6 +652,8 @@ { bool skip_redisplay = true; + octave_link::clear_screen (); + octave::command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1060,7 +1073,7 @@ Fdrawnow (); - int c = octave::kbhit (args.length () == 0); + int c = octave::kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/sysdep.h Thu Jan 02 06:37:54 2020 -0600 @@ -46,7 +46,7 @@ extern OCTINTERP_API int pclose (FILE *f); - extern OCTINTERP_API int kbhit (bool wait = true); + extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); @@ -102,7 +102,7 @@ inline int octave_kbhit (bool wait = true) { - return octave::kbhit (wait); + return octave::kbhit ("", wait); } OCTAVE_DEPRECATED (5, "use 'octave::get_P_tmpdir' instead") diff -r 56dd7419d7aa -r 84cbf166497f libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/corefcn/utils.cc Thu Jan 02 06:37:54 2020 -0600 @@ -1301,7 +1301,7 @@ if (do_graphics_events) gh_manager::process_events (); - c = octave::kbhit (false); + c = octave::kbhit ("press enter to continue", false); } } else diff -r 56dd7419d7aa -r 84cbf166497f libinterp/octave.cc --- a/libinterp/octave.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/octave.cc Thu Jan 02 06:37:54 2020 -0600 @@ -44,6 +44,7 @@ #include "octave.h" #include "oct-hist.h" #include "oct-map.h" +#include "octave-link.h" #include "ovl.h" #include "options-usage.h" #include "ov.h" @@ -184,6 +185,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -431,7 +442,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (octave::application::is_gui_running ()); + return ovl (octave_link::enabled ()); } /* diff -r 56dd7419d7aa -r 84cbf166497f libinterp/octave.h --- a/libinterp/octave.h Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/octave.h Thu Jan 02 06:37:54 2020 -0600 @@ -84,6 +84,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -120,6 +122,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -226,6 +230,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; diff -r 56dd7419d7aa -r 84cbf166497f libinterp/options-usage.h --- a/libinterp/options-usage.h Tue Dec 24 04:01:33 2019 +0100 +++ b/libinterp/options-usage.h Thu Jan 02 06:37:54 2020 -0600 @@ -35,10 +35,10 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--line-editing] [--no-gui] [--no-history]\n\ - [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--silent] [--traditional]\n\ + [--jit-compiler] [--json-sock] [--json-max-len] [--line-editing]\n\ + [--no-gui] [--no-history][--no-init-file] [--no-init-path]\n\ + [--no-line-editing] [--no-site-file] [--no-window-system] [--norc]\n\ + [-p path] [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; // This is here so that it's more likely that the usage message and @@ -67,15 +67,17 @@ #define INFO_PROG_OPTION 8 #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 -#define LINE_EDITING_OPTION 11 -#define NO_GUI_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define TEXI_MACROS_FILE_OPTION 18 -#define TRADITIONAL_OPTION 19 +#define JSON_SOCK_OPTION 11 +#define JSON_MAX_LEN_OPTION 12 +#define LINE_EDITING_OPTION 13 +#define NO_GUI_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define TEXI_MACROS_FILE_OPTION 20 +#define TRADITIONAL_OPTION 21 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, 0, TRADITIONAL_OPTION }, @@ -94,6 +96,8 @@ { "info-program", octave_required_arg, 0, INFO_PROG_OPTION }, { "interactive", octave_no_arg, 0, 'i' }, { "jit-compiler", octave_no_arg, 0, JIT_COMPILER_OPTION }, + { "json-sock", octave_required_arg, 0, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, 0, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, 0, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, 0, NO_GUI_OPTION }, { "no-history", octave_no_arg, 0, 'H' }, @@ -145,6 +149,8 @@ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 56dd7419d7aa -r 84cbf166497f liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/liboctave/util/oct-mutex.cc Thu Jan 02 06:37:54 2020 -0600 @@ -55,6 +55,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -65,11 +77,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -87,8 +101,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -120,11 +145,18 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -142,8 +174,19 @@ return (pthread_mutex_trylock (&pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 56dd7419d7aa -r 84cbf166497f liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Tue Dec 24 04:01:33 2019 +0100 +++ b/liboctave/util/oct-mutex.h Thu Jan 02 06:37:54 2020 -0600 @@ -47,6 +47,10 @@ virtual bool try_lock (void); + virtual void cond_wait (void); + + virtual void cond_signal (void); + private: refcount count; }; @@ -99,6 +103,16 @@ return rep->try_lock (); } + void cond_wait (void) + { + rep->cond_wait (); + } + + void cond_signal (void) + { + rep->cond_signal (); + } + protected: base_mutex *rep; }; diff -r 56dd7419d7aa -r 84cbf166497f liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Tue Dec 24 04:01:33 2019 +0100 +++ b/liboctave/util/url-transfer.cc Thu Jan 02 06:37:54 2020 -0600 @@ -38,6 +38,8 @@ #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" +#include "libinterp/corefcn/octave-link.h" +#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" @@ -237,6 +239,86 @@ return file_list; } + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform (param, "get"); + } + + void http_post (const Array& param) { + perform (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform (param, action); + } + +private: + void perform(const Array& param, const std::string& action) { + std::string url = host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (octave_link::request_input_enabled ()) { + bool success; + std::string result = octave_link::request_url (url, paramList, action, success); + if (success) { + process_success(result); + } else { + ok = false; + errmsg = result; + } + } else { + ok = false; + errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + ok = false; + errmsg = "failed decoding base64 from octave_link"; + } else { + curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -806,17 +888,30 @@ # define REP_CLASS base_url_transfer #endif - url_transfer::url_transfer (void) : rep (new REP_CLASS ()) - { } + url_transfer::url_transfer (void) { + if (octave_link::request_input_enabled()) { + rep = new link_transfer(); + } else { + rep = new REP_CLASS(); + } + } url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : rep (new REP_CLASS (host, user, passwd, os)) - { } + const std::string& passwd, std::ostream& os) { + if (octave_link::request_input_enabled()) { + rep = new link_transfer(host, user, passwd, os); + } else { + rep = new REP_CLASS(host, user, passwd, os); + } + } - url_transfer::url_transfer (const std::string& url, std::ostream& os) - : rep (new REP_CLASS (url, os)) - { } + url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (octave_link::request_input_enabled()) { + rep = new link_transfer(url, os); + } else { + rep = new REP_CLASS(url, os); + } + } #undef REP_CLASS diff -r 56dd7419d7aa -r 84cbf166497f scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Tue Dec 24 04:01:33 2019 +0100 +++ b/scripts/help/__unimplemented__.m Thu Jan 02 06:37:54 2020 -0600 @@ -40,7 +40,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -514,6 +537,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -556,13 +580,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run `pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 56dd7419d7aa -r 84cbf166497f scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Tue Dec 24 04:01:33 2019 +0100 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Thu Jan 02 06:37:54 2020 -0600 @@ -27,9 +27,84 @@ if (nargin < 1 || nargin > 4 || nargin == 2) print_usage (); + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __octave_link_plot_destination__ () == 0 || + __octave_link_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif + + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __octave_link_plot_destination__ () == 1 || + __octave_link_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __octave_link_show_static_plot__ (term, a); + endif + endif + endif + endif +endfunction - if (nargin >= 3 && nargin <= 4) +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -65,44 +140,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/200-README.md ================================================ Patch file `200` is the data on the oo-4.2.1 branch applied against Octave 5.2 RC and saved in a new branch oo-5.2. It is based on commit `56dd7419d7aa` on the stable branch. ================================================ FILE: back-octave/oo-changesets/201-b993253f19d0.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1578034524 21600 # Fri Jan 03 00:55:24 2020 -0600 # Branch oo-5.2 # Node ID b993253f19d07cc1e9373cd8e9a35745cde9fef9 # Parent 84cbf166497fa3d942be6a0b026ec24521301d16 Updating OO patch code to be 5.2-compatible diff -r 84cbf166497f -r b993253f19d0 libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Thu Jan 02 06:37:54 2020 -0600 +++ b/libinterp/corefcn/interpreter.cc Fri Jan 03 00:55:24 2020 -0600 @@ -510,6 +510,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } m_input_system.initialize (line_editing); @@ -519,11 +524,6 @@ initialize_version_info (); - if (!options.json_sock_path().empty ()) { - static json_main _json_main (options.json_sock_path(), options.json_max_message_length()); - _json_main.run_loop_on_new_thread(); - } - // This should be done before initializing the load path because // some PKG_ADD files might need --traditional behavior. diff -r 84cbf166497f -r b993253f19d0 libinterp/corefcn/json-util.cc --- a/libinterp/corefcn/json-util.cc Thu Jan 02 06:37:54 2020 -0600 +++ b/libinterp/corefcn/json-util.cc Fri Jan 03 00:55:24 2020 -0600 @@ -3,17 +3,107 @@ #endif #include +#include #include #include #include #include +#include #include "str-vec.h" #include "json-util.h" JSON_OBJECT_T json_util::from_string(const std::string& str) { - return json_object_new_string(str.c_str()); + const char* snowflake = "\xEF\xBF\xBD"; + + // Ensure that the string is valid UTF-8 + std::string sanitized; + size_t state = 0; + size_t cpLength = 0; + for (size_t i=0; i= 0xC2 && c <= 0xDF) { + // 2-byte character + state = 1; + cpLength = 2; + } else if (c >= 0xE0 && c <= 0xEF) { + // 3-byte character + state = 1; + cpLength = 3; + } else if (c >= 0xF0 && c <= 0xF4) { + // 4-byte character + state = 1; + cpLength = 4; + } else { + // Invalid byte + sanitized.append(snowflake); + } + break; + + case 1: + if (c < 0x80 || c > 0xBF) { + // Invalid byte + sanitized.append(snowflake); + state = 0; + } else if (cpLength == 2) { + // Final byte in 2-byte character + sanitized.push_back(str[i-1]); + sanitized.push_back(c); + state = 0; + } else { + // 3-byte or 4-byte character + state = 2; + } + break; + + case 2: + if (c < 0x80 || c > 0xBF) { + // Invalid byte + sanitized.append(snowflake); + state = 0; + } else if (cpLength == 3) { + // Final byte in 3-byte character + sanitized.push_back(str[i-2]); + sanitized.push_back(str[i-1]); + sanitized.push_back(c); + state = 0; + } else { + // 4-byte character + state = 3; + } + break; + + case 3: + if (c < 0x80 || c > 0xBF) { + // Invalid byte + sanitized.append(snowflake); + state = 0; + } else { + assert(cpLength == 4); + sanitized.push_back(str[i-3]); + sanitized.push_back(str[i-2]); + sanitized.push_back(str[i-1]); + sanitized.push_back(c); + state = 0; + } + break; + + } + } + + if (state != 0) { + // Last character is invalid + sanitized.append(snowflake); + state = 0; + } + + return json_object_new_string_len(sanitized.c_str(), sanitized.length()); } JSON_OBJECT_T json_util::from_int(int i) { @@ -36,7 +126,7 @@ JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { JSON_OBJECT_T jobj = json_object_new_array(); for ( - typename std::list::const_iterator it = list.begin(); + auto it = list.begin(); it != list.end(); ++it ){ @@ -67,8 +157,16 @@ return json_object_from_list(list, json_util::from_float); } -JSON_OBJECT_T json_util::from_workspace_list(const std::list& list) { - return json_object_from_list(list, json_util::from_workspace_element); +JSON_OBJECT_T json_util::from_symbol_info_list(const octave::symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; } JSON_OBJECT_T json_util::from_filter_list(const octave_link::filter_list& list) { @@ -79,21 +177,28 @@ return json_util::from_string(str); } -JSON_OBJECT_T json_util::from_workspace_element(workspace_element element) { +JSON_OBJECT_T json_util::from_symbol_info(const octave::symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + JSON_MAP_T m; - m["scope"] = json_util::from_int(element.scope()); - m["symbol"] = json_util::from_string(element.symbol()); - m["class_name"] = json_util::from_string(element.class_name()); - m["dimension"] = json_util::from_string(element.dimension()); - m["value"] = json_util::from_string(element.value()); - m["complex_flag"] = json_util::from_boolean(element.complex_flag()); + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); return json_util::from_map(m); } JSON_OBJECT_T json_util::from_pair(std::pair pair) { JSON_OBJECT_T jobj = json_object_new_array(); - json_object_array_add(jobj, json_object_new_string(pair.first.c_str())); - json_object_array_add(jobj, json_object_new_string(pair.second.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); return jobj; } @@ -129,7 +234,7 @@ if (arr == NULL) return ret; - for (int i = 0; i < array_list_length(arr); ++i) { + for (size_t i = 0; i < array_list_length(arr); ++i) { JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); ret.push_back(convert(jsub)); } diff -r 84cbf166497f -r b993253f19d0 libinterp/corefcn/json-util.h --- a/libinterp/corefcn/json-util.h Thu Jan 02 06:37:54 2020 -0600 +++ b/libinterp/corefcn/json-util.h Fri Jan 03 00:55:24 2020 -0600 @@ -5,7 +5,7 @@ #include #include -#include "workspace-element.h" +#include "syminfo.h" #include "octave-link.h" class string_vector; @@ -33,11 +33,11 @@ static JSON_OBJECT_T from_string_vector(const string_vector& list); static JSON_OBJECT_T from_int_list(const std::list& list); static JSON_OBJECT_T from_float_list(const std::list& list); - static JSON_OBJECT_T from_workspace_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const octave::symbol_info_list& list); static JSON_OBJECT_T from_filter_list(const octave_link::filter_list& list); static JSON_OBJECT_T from_value_string(const std::string str); - static JSON_OBJECT_T from_workspace_element(workspace_element element); + static JSON_OBJECT_T from_symbol_info(const octave::symbol_info element); static JSON_OBJECT_T from_pair(std::pair pair); static JSON_OBJECT_T from_map(JSON_MAP_T m); diff -r 84cbf166497f -r b993253f19d0 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Thu Jan 02 06:37:54 2020 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Fri Jan 03 00:55:24 2020 -0600 @@ -26,7 +26,6 @@ #include #include "octave-json-link.h" -#include "workspace-element.h" #include "cmd-edit.h" #include "json-main.h" #include "json-util.h" @@ -73,17 +72,18 @@ return confirm_shutdown_queue.dequeue(); } -bool octave_json_link::do_exit(int status) { - JSON_MAP_T m; - JSON_MAP_SET(m, status, int); - _publish_message("exit", json_util::from_map(m)); +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); - // It is our responsibility in octave_link to call exit. If we don't, then - // the kernel waits for 24 hours expecting us to do something. - ::exit(status); +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); - return true; -} +// return true; +// } bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { // This endpoint might be unused? (References appear only in libgui) @@ -112,16 +112,16 @@ return prompt_new_edit_file_queue.dequeue(); } -int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { - // Triggered in "msgbox", "helpdlg", and "errordlg", among others - JSON_MAP_T m; - JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); - JSON_MAP_SET(m, msg, string); - JSON_MAP_SET(m, title, string); - _publish_message("message-dialog", json_util::from_map(m)); +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); - return message_dialog_queue.dequeue(); -} +// return message_dialog_queue.dequeue(); +// } std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { // Triggered in "questdlg" @@ -198,6 +198,19 @@ _publish_message("change-directory", json_util::from_map(m)); } +void octave_json_link::do_file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::do_file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + void octave_json_link::do_execute_command_in_terminal(const std::string& command) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; @@ -205,13 +218,22 @@ _publish_message("execute-command-in-terminal", json_util::from_map(m)); } -void octave_json_link::do_set_workspace(bool top_level, bool debug, const std::list& ws /*, const bool& variable_editor_too */) { +uint8NDArray octave_json_link::do_get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::do_set_workspace(bool top_level, bool debug, + const octave::symbol_info_list& ws, + bool update_variable_editor) { // Triggered on every new line entry JSON_MAP_T m; JSON_MAP_SET(m, top_level, boolean); JSON_MAP_SET(m, debug, boolean); - JSON_MAP_SET(m, ws, workspace_list); - // variable_editor_too? + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); _publish_message("set-workspace", json_util::from_map(m)); } @@ -279,29 +301,49 @@ _publish_message("update-breakpoint", json_util::from_map(m)); } -void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { - // Triggered upon interpreter startup - JSON_MAP_T m; - JSON_MAP_SET(m, ps1, string); - JSON_MAP_SET(m, ps2, string); - JSON_MAP_SET(m, ps4, string); - _publish_message("set-default-prompts", json_util::from_map(m)); -} +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } void octave_json_link::do_show_preferences(void) { // Triggered on "preferences" command _publish_message("show-preferences", json_util::empty()); } +std::string octave_json_link::do_gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + void octave_json_link::do_show_doc(const std::string& file) { // Triggered on "doc" command _publish_message("show-doc", json_util::from_string(file)); } -// void octave_json_link::do_openvar(const std::string& name) { -// // Triggered on "openvar" command -// _publish_message("openvar", json_util::from_string(name)); -// } +void octave_json_link::do_register_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::do_unregister_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::do_edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { // Triggered on all plot commands with setenv("GNUTERM","svg") diff -r 84cbf166497f -r b993253f19d0 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Thu Jan 02 06:37:54 2020 -0600 +++ b/libinterp/corefcn/octave-json-link.h Fri Jan 03 00:55:24 2020 -0600 @@ -45,7 +45,7 @@ private: std::queue _queue; - octave_mutex _mutex; + octave::mutex _mutex; }; class octave_json_link : public octave_link @@ -61,16 +61,12 @@ std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; bool do_confirm_shutdown (void) override; - bool do_exit (int status) override; bool do_copy_image_to_clipboard (const std::string& file) override; bool do_edit_file (const std::string& file) override; bool do_prompt_new_edit_file (const std::string& file) override; - int do_message_dialog (const std::string& dlg, const std::string& msg, - const std::string& title) override; - std::string do_question_dialog (const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, @@ -105,13 +101,16 @@ void do_change_directory (const std::string& dir) override; + void do_file_remove (const std::string& old_name, const std::string& new_name) override; + void do_file_renamed (bool) override; + void do_execute_command_in_terminal (const std::string& command) override; + uint8NDArray do_get_named_icon (const std::string& icon_name) override; + void do_set_workspace (bool top_level, bool debug, - const std::list& ws - // Added on head but not yet in stable: - // , const bool& variable_editor_too = true - ) override; + const octave::symbol_info_list& ws, + bool update_variable_editor) override; void do_clear_workspace (void) override; @@ -132,15 +131,17 @@ const std::string& file, int line, const std::string& cond) override; - void do_set_default_prompts (std::string& ps1, std::string& ps2, - std::string& ps4) override; + void do_show_preferences (void) override; - void do_show_preferences (void) override; + std::string do_gui_preference (const std::string& key, const std::string& value) override; void do_show_doc (const std::string& file) override; - // Added on head but not yet in stable: - // void do_openvar (const std::string& name) override; + void do_register_doc (const std::string& file) override; + + void do_unregister_doc (const std::string& file) override; + + void do_edit_variable (const std::string& name, const octave_value& val) override; void do_show_static_plot (const std::string& term, const std::string& content) override; diff -r 84cbf166497f -r b993253f19d0 libinterp/corefcn/octave-link.h --- a/libinterp/corefcn/octave-link.h Thu Jan 02 06:37:54 2020 -0600 +++ b/libinterp/corefcn/octave-link.h Fri Jan 03 00:55:24 2020 -0600 @@ -31,7 +31,7 @@ #include #include "oct-mutex.h" -#include "octave.h" +#include "libinterp/octave.h" #include "event-queue.h" #include "uint8NDArray.h" diff -r 84cbf166497f -r b993253f19d0 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Thu Jan 02 06:37:54 2020 -0600 +++ b/liboctave/util/url-transfer.cc Fri Jan 03 00:55:24 2020 -0600 @@ -890,26 +890,26 @@ url_transfer::url_transfer (void) { if (octave_link::request_input_enabled()) { - rep = new link_transfer(); + rep.reset(new link_transfer()); } else { - rep = new REP_CLASS(); + rep.reset(new REP_CLASS()); } } url_transfer::url_transfer (const std::string& host, const std::string& user, const std::string& passwd, std::ostream& os) { if (octave_link::request_input_enabled()) { - rep = new link_transfer(host, user, passwd, os); + rep.reset(new link_transfer(host, user, passwd, os)); } else { - rep = new REP_CLASS(host, user, passwd, os); + rep.reset(new REP_CLASS(host, user, passwd, os)); } } url_transfer::url_transfer (const std::string& url, std::ostream& os) { if (octave_link::request_input_enabled()) { - rep = new link_transfer(url, os); + rep.reset(new link_transfer(url, os)); } else { - rep = new REP_CLASS(url, os); + rep.reset(new REP_CLASS(url, os)); } } ================================================ FILE: back-octave/oo-changesets/202-d9d23f97ba78.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1578045495 21600 # Fri Jan 03 03:58:15 2020 -0600 # Branch oo-5.2 # Node ID d9d23f97ba78c49221f476e82b550a59e9f146a0 # Parent b993253f19d07cc1e9373cd8e9a35745cde9fef9 Updating json-main to obey new ownership policy of octave_link diff -r b993253f19d0 -r d9d23f97ba78 libinterp/corefcn/json-main.cc --- a/libinterp/corefcn/json-main.cc Fri Jan 03 00:55:24 2020 -0600 +++ b/libinterp/corefcn/json-main.cc Fri Jan 03 03:58:15 2020 -0600 @@ -28,11 +28,12 @@ : _json_sock_path (json_sock_path), _max_message_length (max_message_length), _loop_thread_active (false), - _octave_json_link (this) + _octave_json_link (new octave_json_link(this)) { - // Enable octave_json_link instance - octave_link::connect_link(&_octave_json_link); - + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + octave_link::connect_link(_octave_json_link); +j // Open UNIX socket file descriptor sockfd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr; @@ -89,5 +90,5 @@ } void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { - _octave_json_link.receive_message(name, jobj); + _octave_json_link->receive_message(name, jobj); } diff -r b993253f19d0 -r d9d23f97ba78 libinterp/corefcn/json-main.h --- a/libinterp/corefcn/json-main.h Fri Jan 03 00:55:24 2020 -0600 +++ b/libinterp/corefcn/json-main.h Fri Jan 03 03:58:15 2020 -0600 @@ -24,7 +24,9 @@ int sockfd; bool _loop_thread_active; pthread_t _loop_thread; - octave_json_link _octave_json_link; + + // Owned by octave_link + octave_json_link* _octave_json_link; }; #endif ================================================ FILE: back-octave/oo-changesets/203-d6b5ffb8e4cc.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1578047258 21600 # Fri Jan 03 04:27:38 2020 -0600 # Branch oo-5.2 # Node ID d6b5ffb8e4cc940b7337575d0194d025c7385b24 # Parent d9d23f97ba78c49221f476e82b550a59e9f146a0 Remove extra character diff -r d9d23f97ba78 -r d6b5ffb8e4cc libinterp/corefcn/json-main.cc --- a/libinterp/corefcn/json-main.cc Fri Jan 03 03:58:15 2020 -0600 +++ b/libinterp/corefcn/json-main.cc Fri Jan 03 04:27:38 2020 -0600 @@ -33,7 +33,7 @@ // Enable the octave_json_link instance // Note: this passes ownership to octave_link octave_link::connect_link(_octave_json_link); -j + // Open UNIX socket file descriptor sockfd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr; ================================================ FILE: back-octave/oo-changesets/204-e61d7b8918e2.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1578122988 21600 # Sat Jan 04 01:29:48 2020 -0600 # Branch oo-5.2 # Node ID e61d7b8918e2632d23f537cd679e1f532ed317d7 # Parent d6b5ffb8e4cc940b7337575d0194d025c7385b24 Removing UTF-8 logic from json-util.cc diff -r d6b5ffb8e4cc -r e61d7b8918e2 libinterp/corefcn/json-util.cc --- a/libinterp/corefcn/json-util.cc Fri Jan 03 04:27:38 2020 -0600 +++ b/libinterp/corefcn/json-util.cc Sat Jan 04 01:29:48 2020 -0600 @@ -15,95 +15,8 @@ #include "json-util.h" JSON_OBJECT_T json_util::from_string(const std::string& str) { - const char* snowflake = "\xEF\xBF\xBD"; - - // Ensure that the string is valid UTF-8 - std::string sanitized; - size_t state = 0; - size_t cpLength = 0; - for (size_t i=0; i= 0xC2 && c <= 0xDF) { - // 2-byte character - state = 1; - cpLength = 2; - } else if (c >= 0xE0 && c <= 0xEF) { - // 3-byte character - state = 1; - cpLength = 3; - } else if (c >= 0xF0 && c <= 0xF4) { - // 4-byte character - state = 1; - cpLength = 4; - } else { - // Invalid byte - sanitized.append(snowflake); - } - break; - - case 1: - if (c < 0x80 || c > 0xBF) { - // Invalid byte - sanitized.append(snowflake); - state = 0; - } else if (cpLength == 2) { - // Final byte in 2-byte character - sanitized.push_back(str[i-1]); - sanitized.push_back(c); - state = 0; - } else { - // 3-byte or 4-byte character - state = 2; - } - break; - - case 2: - if (c < 0x80 || c > 0xBF) { - // Invalid byte - sanitized.append(snowflake); - state = 0; - } else if (cpLength == 3) { - // Final byte in 3-byte character - sanitized.push_back(str[i-2]); - sanitized.push_back(str[i-1]); - sanitized.push_back(c); - state = 0; - } else { - // 4-byte character - state = 3; - } - break; - - case 3: - if (c < 0x80 || c > 0xBF) { - // Invalid byte - sanitized.append(snowflake); - state = 0; - } else { - assert(cpLength == 4); - sanitized.push_back(str[i-3]); - sanitized.push_back(str[i-2]); - sanitized.push_back(str[i-1]); - sanitized.push_back(c); - state = 0; - } - break; - - } - } - - if (state != 0) { - // Last character is invalid - sanitized.append(snowflake); - state = 0; - } - - return json_object_new_string_len(sanitized.c_str(), sanitized.length()); + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); } JSON_OBJECT_T json_util::from_int(int i) { ================================================ FILE: back-octave/oo-changesets/300-d78448f9c483.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1593731318 18000 # Thu Jul 02 18:08:38 2020 -0500 # Branch oo-5.2a # Node ID d78448f9c48344a58f5f83d5d52c29bf1ad92cb7 # Parent 9e7b2625e5744cfbb01bdd67a3487c75a7ef957a # Parent e61d7b8918e2632d23f537cd679e1f532ed317d7 Merge oo-5.2 into stable diff -r 9e7b2625e574 -r d78448f9c483 configure.ac --- a/configure.ac Fri Jun 26 18:44:35 2020 +0200 +++ b/configure.ac Thu Jul 02 18:08:38 2020 -0500 @@ -2812,7 +2812,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $SPARSE_XLDFLAGS $FFTW_XLDFLAGS $LLVM_LDFLAGS" diff -r 9e7b2625e574 -r d78448f9c483 libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libgui/src/qt-interpreter-events.cc Thu Jul 02 18:08:38 2020 -0500 @@ -261,6 +261,21 @@ emit edit_variable_signal (QString::fromStdString (expr), val); } + void qt_interpreter_events::show_static_plot (const std::string&, const std::string&) + { + return; + } + + std::string qt_interpreter_events::request_input (const std::string&) + { + return {}; + } + + std::string qt_interpreter_events::request_url (const std::string&, const std::list&, const std::string&, bool&) + { + return {}; + } + bool qt_interpreter_events::confirm_shutdown (void) { QMutexLocker autolock (&m_mutex); @@ -505,6 +520,9 @@ emit clear_history_signal (); } + void qt_interpreter_events::do_clear_screen (void) + { } + void qt_interpreter_events::pre_input_event (void) { } diff -r 9e7b2625e574 -r d78448f9c483 libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h Fri Jun 26 18:44:35 2020 +0200 +++ b/libgui/src/qt-interpreter-events.h Thu Jul 02 18:08:38 2020 -0500 @@ -118,6 +118,12 @@ void edit_variable (const std::string& name, const octave_value& val); + void show_static_plot (const std::string& term, const std::string& content); + + std::string request_input (const std::string&); + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool confirm_shutdown (void); bool prompt_new_edit_file (const std::string& file); @@ -160,6 +166,8 @@ void clear_history (void); + void clear_screen (void); + void pre_input_event (void); void post_input_event (void); diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/event-manager.cc Thu Jul 02 18:08:38 2020 -0500 @@ -652,3 +652,28 @@ evmgr.focus_window ("workspace"); return ovl (); } + +DEFUN (__octave_link_plot_destination__, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __octave_link_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (octave_link::plot_destination ()); +} + +DEFUN (__octave_link_show_static_plot__, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __octave_link_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (octave_link::show_static_plot (term, content)); +} + diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/event-manager.h Thu Jul 02 18:08:38 2020 -0500 @@ -147,6 +147,13 @@ // confirmation before another action. Could these be reformulated // using the question_dialog action? + bool _request_input_enabled; + virtual std::string request_input (const std::string&) = 0; + virtual std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + + plot_destination_t _plot_destination; + virtual void show_static_plot (const std::string& term, const std::string& content) = 0; + virtual bool confirm_shutdown (void) { return false; } virtual bool prompt_new_edit_file (const std::string& /*file*/) @@ -220,6 +227,8 @@ virtual void clear_history (void) { } + virtual void clear_screen (void) { } + virtual void pre_input_event (void) { } virtual void post_input_event (void) { } @@ -373,6 +382,34 @@ instance->update_path_dialog (); } + bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + + plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->show_static_plot (term, content); + return true; + } + else + return false; + } + bool show_preferences (void) { if (enabled ()) @@ -551,6 +588,12 @@ instance->clear_history (); } + void clear_screen (void) + { + if (enabled ()) + instance->clear_screen (); + } + void pre_input_event (void) { if (enabled ()) @@ -563,6 +606,21 @@ instance->post_input_event (); } + + std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->request_input (prompt) + : std::string (); + } + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->request_url (url, param, action, success) + : std::string (); + } + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) { diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/input.cc Thu Jul 02 18:08:38 2020 -0500 @@ -671,7 +671,12 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + event_manager& evmgr = m_interpreter.get_event_manager (); + if (evmgr.request_input_enabled ()) + retval = evmgr.request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1443,3 +1448,32 @@ } // #endif + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/interpreter.cc Thu Jul 02 18:08:38 2020 -0500 @@ -59,6 +59,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave.h" @@ -600,6 +601,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (*this, options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } // FIXME: we defer creation of the gh_manager object because it diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Thu Jul 02 18:08:38 2020 -0500 @@ -0,0 +1,96 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (new octave_json_link(this)) +{ + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + event_manager& evmgr = interp.get_event_manager (); + evmgr.connect_link (_octave_json_link); + evmgr.enable (); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link->receive_message(name, jobj); +} diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Thu Jul 02 18:08:38 2020 -0500 @@ -0,0 +1,32 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +class json_main { +public: + json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + + // Owned by octave_link + octave_json_link* _octave_json_link; +}; + +#endif diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Thu Jul 02 18:08:38 2020 -0500 @@ -0,0 +1,260 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_symbol_info_list(const octave::symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_filter_list(const octave_link::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_symbol_info(const octave::symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + + JSON_MAP_T m; + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (size_t i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Thu Jul 02 18:08:38 2020 -0500 @@ -0,0 +1,60 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "syminfo.h" +#include "event-manager.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const octave::symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const event_manager::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_symbol_info(const octave::symbol_info element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +#endif diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/module.mk Thu Jul 02 18:08:38 2020 -0500 @@ -45,6 +45,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/load-path.h \ %reldir%/load-save.h \ @@ -73,6 +75,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ %reldir%/pr-output.h \ @@ -182,6 +185,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ %reldir%/inv.cc \ @@ -218,6 +223,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ %reldir%/pinv.cc \ diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Thu Jul 02 18:08:38 2020 -0500 @@ -0,0 +1,407 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +octave_json_link::octave_json_link(json_main* __json_main) + : interpreter_events (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::do_request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::do_request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::do_confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); + +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); + +// return true; +// } + +bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::do_prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); + +// return message_dialog_queue.dequeue(); +// } + +std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::do_list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::do_file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::do_debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::do_change_directory(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::do_file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::do_file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + +void octave_json_link::do_execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +uint8NDArray octave_json_link::do_get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::do_set_workspace(bool top_level, bool debug, + const octave::symbol_info_list& ws, + bool update_variable_editor) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::do_clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::do_set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::do_append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::do_clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::do_clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::do_pre_input_event(void) { + // noop +} + +void octave_json_link::do_post_input_event(void) { + // noop +} + +void octave_json_link::do_enter_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::do_exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::do_update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } + +void octave_json_link::do_show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +std::string octave_json_link::do_gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + +void octave_json_link::do_show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +void octave_json_link::do_register_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::do_unregister_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::do_edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} + +void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = octave::command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Thu Jul 02 18:08:38 2020 -0500 @@ -0,0 +1,210 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "event-manager.h" +#include "json-util.h" +#include "oct-mutex.h" + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + octave::mutex _mutex; +}; + +class octave_json_link : public interpreter_events +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string do_request_input (const std::string& prompt) override; + std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + bool do_confirm_shutdown (void) override; + + bool do_copy_image_to_clipboard (const std::string& file) override; + + bool do_edit_file (const std::string& file) override; + bool do_prompt_new_edit_file (const std::string& file) override; + + std::string + do_question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + do_list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + do_input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + do_file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + int + do_debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + void do_change_directory (const std::string& dir) override; + + void do_file_remove (const std::string& old_name, const std::string& new_name) override; + void do_file_renamed (bool) override; + + void do_execute_command_in_terminal (const std::string& command) override; + + uint8NDArray do_get_named_icon (const std::string& icon_name) override; + + void do_set_workspace (bool top_level, bool debug, + const octave::symbol_info_list& ws, + bool update_variable_editor) override; + + void do_clear_workspace (void) override; + + void do_set_history (const string_vector& hist) override; + void do_append_history (const std::string& hist_entry) override; + void do_clear_history (void) override; + + void do_clear_screen (void) override; + + void do_pre_input_event (void) override; + void do_post_input_event (void) override; + + void do_enter_debugger_event (const std::string& file, int line) override; + void do_execute_in_debugger_event (const std::string& file, int line) override; + void do_exit_debugger_event (void) override; + + void do_update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + void do_show_preferences (void) override; + + std::string do_gui_preference (const std::string& key, const std::string& value) override; + + void do_show_doc (const std::string& file) override; + + void do_register_doc (const std::string& file) override; + + void do_unregister_doc (const std::string& file) override; + + void do_edit_variable (const std::string& name, const octave_value& val) override; + + void do_show_static_plot (const std::string& term, + const std::string& content) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +#endif diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/syscalls.cc Thu Jul 02 18:08:38 2020 -0500 @@ -149,9 +149,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -461,42 +460,6 @@ return ovl (status, msg); } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (interp.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = octave::sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/sysdep.cc Thu Jul 02 18:08:38 2020 -0500 @@ -75,6 +75,7 @@ #include "defun.h" #include "error.h" #include "errwarn.h" +#include "event-manager.h" #include "input.h" #include "interpreter-private.h" #include "octave.h" @@ -651,7 +652,7 @@ // Read one character from the terminal. - int kbhit (bool wait) + int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -678,13 +679,24 @@ set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + event_manager& evmgr = __get_event_manager__ ("kbhit"); + if (evmgr.request_input_enabled ()) { + std::string line = evmgr.request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). set_interrupt_handler (saved_interrupt_handler, true); @@ -743,6 +755,8 @@ { bool skip_redisplay = true; + octave_link::clear_screen (); + octave::command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1169,7 +1183,7 @@ Fdrawnow (interp); - int c = octave::kbhit (args.length () == 0); + int c = octave::kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/sysdep.h Thu Jul 02 18:08:38 2020 -0500 @@ -49,7 +49,7 @@ extern OCTINTERP_API int pclose (FILE *f); - extern OCTINTERP_API int kbhit (bool wait = true); + extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); @@ -107,7 +107,7 @@ inline int octave_kbhit (bool wait = true) { - return octave::kbhit (wait); + return octave::kbhit ("", wait); } OCTAVE_DEPRECATED (5, "use 'octave::get_P_tmpdir' instead") diff -r 9e7b2625e574 -r d78448f9c483 libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/corefcn/utils.cc Thu Jul 02 18:08:38 2020 -0500 @@ -1442,7 +1442,7 @@ if (do_graphics_events) gh_mgr.process_events (); - c = kbhit (false); + c = kbhit ("press enter to continue", false); } } else diff -r 9e7b2625e574 -r d78448f9c483 libinterp/octave.cc --- a/libinterp/octave.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/octave.cc Thu Jul 02 18:08:38 2020 -0500 @@ -187,6 +187,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -397,7 +407,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (octave::application::is_gui_running ()); + return ovl (octave::application::is_link_enabled ()); } /* diff -r 9e7b2625e574 -r d78448f9c483 libinterp/octave.h --- a/libinterp/octave.h Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/octave.h Thu Jul 02 18:08:38 2020 -0500 @@ -79,6 +79,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -109,6 +111,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -215,6 +219,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; @@ -305,6 +317,14 @@ return instance ? instance->gui_running () : false; } + static bool is_link_enabled (void) + { + if (instance && instance->m_interpreter) { + event_manager& evmgr = instance->m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; + } + // Convenience functions. static bool forced_interactive (void); diff -r 9e7b2625e574 -r d78448f9c483 libinterp/options-usage.h --- a/libinterp/options-usage.h Fri Jun 26 18:44:35 2020 +0200 +++ b/libinterp/options-usage.h Thu Jul 02 18:08:38 2020 -0500 @@ -38,10 +38,10 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--line-editing] [--no-gui] [--no-history]\n\ - [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--silent] [--traditional]\n\ + [--jit-compiler] [--json-sock] [--json-max-len] [--line-editing]\n\ + [--no-gui] [--no-history][--no-init-file] [--no-init-path]\n\ + [--no-line-editing] [--no-site-file] [--no-window-system] [--norc]\n\ + [-p path] [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; // This is here so that it's more likely that the usage message and @@ -68,15 +68,17 @@ #define INFO_PROG_OPTION 8 #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 -#define LINE_EDITING_OPTION 11 -#define NO_GUI_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define TEXI_MACROS_FILE_OPTION 18 -#define TRADITIONAL_OPTION 19 +#define JSON_SOCK_OPTION 11 +#define JSON_MAX_LEN_OPTION 12 +#define LINE_EDITING_OPTION 13 +#define NO_GUI_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define TEXI_MACROS_FILE_OPTION 20 +#define TRADITIONAL_OPTION 21 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, 0, TRADITIONAL_OPTION }, @@ -94,6 +96,8 @@ { "info-program", octave_required_arg, 0, INFO_PROG_OPTION }, { "interactive", octave_no_arg, 0, 'i' }, { "jit-compiler", octave_no_arg, 0, JIT_COMPILER_OPTION }, + { "json-sock", octave_required_arg, 0, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, 0, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, 0, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, 0, NO_GUI_OPTION }, { "no-history", octave_no_arg, 0, 'H' }, @@ -145,6 +149,8 @@ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 9e7b2625e574 -r d78448f9c483 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/liboctave/util/oct-mutex.cc Thu Jul 02 18:08:38 2020 -0500 @@ -58,6 +58,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -68,11 +80,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -90,8 +104,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -123,11 +148,19 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&m_pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&m_pm); + pthread_mutex_destroy (&m_pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -145,8 +178,19 @@ return (pthread_mutex_trylock (&m_pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t m_pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 9e7b2625e574 -r d78448f9c483 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Fri Jun 26 18:44:35 2020 +0200 +++ b/liboctave/util/oct-mutex.h Thu Jul 02 18:08:38 2020 -0500 @@ -50,6 +50,10 @@ virtual bool try_lock (void); + virtual void cond_wait (void); + + virtual void cond_signal (void); + private: refcount m_count; }; @@ -102,6 +106,16 @@ return m_rep->try_lock (); } + void cond_wait (void) + { + rep->cond_wait (); + } + + void cond_signal (void) + { + rep->cond_signal (); + } + protected: base_mutex *m_rep; }; diff -r 9e7b2625e574 -r d78448f9c483 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Fri Jun 26 18:44:35 2020 +0200 +++ b/liboctave/util/url-transfer.cc Thu Jul 02 18:08:38 2020 -0500 @@ -36,6 +36,8 @@ #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" +#include "libinterp/corefcn/interpreter-private.h" +#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" #include "version.h" @@ -240,6 +242,87 @@ return file_list; } + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform (param, "get"); + } + + void http_post (const Array& param) { + perform (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform (param, action); + } + +private: + void perform(const Array& param, const std::string& action) { + std::string url = host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + event_manager& evmgr = __get_event_manager__ ("link_transfer"); + if (evmgr.request_input_enabled ()) { + bool success; + std::string result = evmgr.request_url (url, paramList, action, success); + if (success) { + process_success(result); + } else { + ok = false; + errmsg = result; + } + } else { + ok = false; + errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + ok = false; + errmsg = "failed decoding base64 from octave_link"; + } else { + curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -941,17 +1024,33 @@ # define REP_CLASS base_url_transfer #endif - url_transfer::url_transfer (void) : m_rep (new REP_CLASS ()) - { } + url_transfer::url_transfer (void) { + event_manager& evmgr = __get_event_manager__ ("url_transfer"); + if (evmgr.request_input_enabled()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new REP_CLASS()); + } + } url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : m_rep (new REP_CLASS (host, user, passwd, os)) - { } + const std::string& passwd, std::ostream& os) { + event_manager& evmgr = __get_event_manager__ ("url_transfer"); + if (evmgr.request_input_enabled()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new REP_CLASS(host, user, passwd, os)); + } + } - url_transfer::url_transfer (const std::string& url, std::ostream& os) - : m_rep (new REP_CLASS (url, os)) - { } + url_transfer::url_transfer (const std::string& url, std::ostream& os) { + event_manager& evmgr = __get_event_manager__ ("url_transfer"); + if (evmgr.request_input_enabled()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new REP_CLASS(url, os)); + } + } #undef REP_CLASS diff -r 9e7b2625e574 -r d78448f9c483 scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Fri Jun 26 18:44:35 2020 +0200 +++ b/scripts/help/__unimplemented__.m Thu Jul 02 18:08:38 2020 -0500 @@ -45,7 +45,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -524,6 +547,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -566,13 +590,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run 'pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 9e7b2625e574 -r d78448f9c483 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Fri Jun 26 18:44:35 2020 +0200 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Thu Jul 02 18:08:38 2020 -0500 @@ -32,9 +32,84 @@ if (nargin < 1 || nargin > 4 || nargin == 2) print_usage (); + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __octave_link_plot_destination__ () == 0 || + __octave_link_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif + + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __octave_link_plot_destination__ () == 1 || + __octave_link_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __octave_link_show_static_plot__ (term, a); + endif + endif + endif + endif +endfunction - if (nargin >= 3 && nargin <= 4) +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -70,44 +145,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/301-97f7d1f4fe83.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1593748718 0 # Fri Jul 03 03:58:38 2020 +0000 # Branch oo-5.2a # Node ID 97f7d1f4fe83bd6ebe102eb4cc6268ae23ad8d52 # Parent d78448f9c48344a58f5f83d5d52c29bf1ad92cb7 Fixing compiler errors diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/event-manager.cc Fri Jul 03 03:58:38 2020 +0000 @@ -41,6 +41,17 @@ namespace octave { + + bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ ("request_input_enabled"); + return evmgr.request_input_enabled(); + } + + std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ ("request_url"); + return evmgr.request_url(url, param, action, success); + } + static int readline_event_hook (void) { event_manager& evmgr = __get_event_manager__ ("octave_readline_hook"); @@ -653,18 +664,18 @@ return ovl (); } -DEFUN (__octave_link_plot_destination__, , , - doc: /* -*- texinfo -*- -@deftypefn {} {} __octave_link_plot_destination__ () +DEFMETHOD (__event_manager_plot_destination__, interp, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_plot_destination__ () Undocumented internal function. @end deftypefn*/) { - return ovl (octave_link::plot_destination ()); + return ovl (interp.get_event_manager().plot_destination()); } -DEFUN (__octave_link_show_static_plot__, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {} __octave_link_show_static_plot__ (@var{term}, @var{content}) +DEFMETHOD (__event_manager_show_static_plot__, interp, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_show_static_plot__ (@var{term}, @var{content}) Undocumented internal function. @end deftypefn*/) { @@ -674,6 +685,6 @@ std::string term = args(0).string_value(); std::string content = args(1).string_value(); - return ovl (octave_link::show_static_plot (term, content)); + return ovl (interp.get_event_manager().show_static_plot(term, content)); } diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/event-manager.h Fri Jul 03 03:58:38 2020 +0000 @@ -48,6 +48,12 @@ class symbol_info_list; + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + // The methods in this class provide a way to pass signals to the GUI // thread. A GUI that wishes to act on these events should derive // from this class and perform actions in a thread-safe way. In @@ -387,12 +393,6 @@ return enabled () ? instance->_request_input_enabled : false; } - enum plot_destination_t { - TERMINAL_ONLY = 0, - STATIC_ONLY = 1, - TERMINAL_AND_STATIC = 2 - }; - plot_destination_t plot_destination (void) { return enabled () ? instance->_plot_destination : TERMINAL_ONLY; diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/json-main.cc --- a/libinterp/corefcn/json-main.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/json-main.cc Fri Jul 03 03:58:38 2020 +0000 @@ -3,6 +3,7 @@ #endif #include "json-main.h" +#include "interpreter.h" #include #include @@ -13,6 +14,8 @@ // Analog of main-window.cc // TODO: Think more about concurrency and null pointer exceptions +namespace octave { + void* run_loop_pthread(void* arg) { json_main* _json_main = static_cast(arg); _json_main->run_loop(); @@ -94,3 +97,5 @@ void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { _octave_json_link->receive_message(name, jobj); } + +} // namespace octave diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/json-main.h --- a/libinterp/corefcn/json-main.h Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/json-main.h Fri Jul 03 03:58:38 2020 +0000 @@ -8,6 +8,10 @@ #include "octave-json-link.h" #include "json-util.h" +namespace octave { + +class interpreter; + class json_main { public: json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); @@ -25,8 +29,9 @@ bool _loop_thread_active; pthread_t _loop_thread; - // Owned by octave_link - octave_json_link* _octave_json_link; + std::shared_ptr _octave_json_link; }; +} // namespace octave + #endif diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/json-util.cc --- a/libinterp/corefcn/json-util.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/json-util.cc Fri Jul 03 03:58:38 2020 +0000 @@ -14,6 +14,8 @@ #include "json-util.h" +namespace octave { + JSON_OBJECT_T json_util::from_string(const std::string& str) { // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. return json_object_new_string_len(str.c_str(), str.length()); @@ -70,7 +72,7 @@ return json_object_from_list(list, json_util::from_float); } -JSON_OBJECT_T json_util::from_symbol_info_list(const octave::symbol_info_list& list) { +JSON_OBJECT_T json_util::from_symbol_info_list(const symbol_info_list& list) { JSON_OBJECT_T jobj = json_object_new_array(); for ( auto it = list.begin(); @@ -82,7 +84,7 @@ return jobj; } -JSON_OBJECT_T json_util::from_filter_list(const octave_link::filter_list& list) { +JSON_OBJECT_T json_util::from_filter_list(const interpreter_events::filter_list& list) { return json_object_from_list(list, json_util::from_pair); } @@ -90,7 +92,7 @@ return json_util::from_string(str); } -JSON_OBJECT_T json_util::from_symbol_info(const octave::symbol_info element) { +JSON_OBJECT_T json_util::from_symbol_info(const symbol_info element) { octave_value val = element.value(); std::string dims_str = val.get_dims_str(); @@ -258,3 +260,5 @@ arg ); } + +} // namespace octave diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/json-util.h --- a/libinterp/corefcn/json-util.h Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/json-util.h Fri Jul 03 03:58:38 2020 +0000 @@ -21,6 +21,8 @@ m[#FIELD] = json_util::from_##TYPE (FIELD); \ } +namespace octave { + class json_util { public: static JSON_OBJECT_T from_string(const std::string& str); @@ -33,11 +35,11 @@ static JSON_OBJECT_T from_string_vector(const string_vector& list); static JSON_OBJECT_T from_int_list(const std::list& list); static JSON_OBJECT_T from_float_list(const std::list& list); - static JSON_OBJECT_T from_symbol_info_list(const octave::symbol_info_list& list); - static JSON_OBJECT_T from_filter_list(const event_manager::filter_list& list); + static JSON_OBJECT_T from_symbol_info_list(const symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const interpreter_events::filter_list& list); static JSON_OBJECT_T from_value_string(const std::string str); - static JSON_OBJECT_T from_symbol_info(const octave::symbol_info element); + static JSON_OBJECT_T from_symbol_info(const symbol_info element); static JSON_OBJECT_T from_pair(std::pair pair); static JSON_OBJECT_T from_map(JSON_MAP_T m); @@ -57,4 +59,6 @@ static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); }; +} // namespace octave + #endif diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/octave-json-link.cc Fri Jul 03 03:58:38 2020 +0000 @@ -30,6 +30,8 @@ #include "json-main.h" #include "json-util.h" +namespace octave { + octave_json_link::octave_json_link(json_main* __json_main) : interpreter_events (), _json_main (__json_main) @@ -40,7 +42,7 @@ octave_json_link::~octave_json_link(void) { } -std::string octave_json_link::do_request_input(const std::string& prompt) { +std::string octave_json_link::request_input(const std::string& prompt) { // Triggered whenever the console prompts for user input std::string value; @@ -51,7 +53,7 @@ return value; } -std::string octave_json_link::do_request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { +std::string octave_json_link::request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { // Triggered on urlread/urlwrite JSON_MAP_T m; @@ -65,7 +67,7 @@ return result.second; } -bool octave_json_link::do_confirm_shutdown(void) { +bool octave_json_link::confirm_shutdown(void) { // Triggered when the kernel tries to exit _publish_message("confirm-shutdown", json_util::empty()); @@ -85,7 +87,7 @@ // return true; // } -bool octave_json_link::do_copy_image_to_clipboard(const std::string& file) { +bool octave_json_link::copy_image_to_clipboard(const std::string& file) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; JSON_MAP_SET(m, file, string); @@ -94,7 +96,7 @@ return true; } -bool octave_json_link::do_edit_file(const std::string& file) { +bool octave_json_link::edit_file(const std::string& file) { // Triggered in "edit" for existing files JSON_MAP_T m; JSON_MAP_SET(m, file, string); @@ -103,7 +105,7 @@ return true; } -bool octave_json_link::do_prompt_new_edit_file(const std::string& file) { +bool octave_json_link::prompt_new_edit_file(const std::string& file) { // Triggered in "edit" for new files JSON_MAP_T m; JSON_MAP_SET(m, file, string); @@ -123,7 +125,7 @@ // return message_dialog_queue.dequeue(); // } -std::string octave_json_link::do_question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { +std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { // Triggered in "questdlg" JSON_MAP_T m; JSON_MAP_SET(m, msg, string); @@ -137,7 +139,7 @@ return question_dialog_queue.dequeue(); } -std::pair, int> octave_json_link::do_list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { +std::pair, int> octave_json_link::list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { // Triggered in "listdlg" JSON_MAP_T m; JSON_MAP_SET(m, list, string_list); @@ -154,7 +156,7 @@ return list_dialog_queue.dequeue(); } -std::list octave_json_link::do_input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { +std::list octave_json_link::input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { // Triggered in "inputdlg" JSON_MAP_T m; JSON_MAP_SET(m, prompt, string_list); @@ -167,7 +169,7 @@ return input_dialog_queue.dequeue(); } -std::list octave_json_link::do_file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { +std::list octave_json_link::file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { // Triggered in "uiputfile", "uigetfile", and "uigetdir" JSON_MAP_T m; JSON_MAP_SET(m, filter, filter_list); @@ -180,7 +182,7 @@ return file_dialog_queue.dequeue(); } -int octave_json_link::do_debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { +int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { // This endpoint might be unused? (No references) JSON_MAP_T m; JSON_MAP_SET(m, file, string); @@ -191,14 +193,14 @@ return debug_cd_or_addpath_error_queue.dequeue(); } -void octave_json_link::do_change_directory(const std::string& dir) { +void octave_json_link::directory_changed(const std::string& dir) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; JSON_MAP_SET(m, dir, string); _publish_message("change-directory", json_util::from_map(m)); } -void octave_json_link::do_file_remove (const std::string& old_name, const std::string& new_name) { +void octave_json_link::file_remove (const std::string& old_name, const std::string& new_name) { // Called by "unlink", "rmdir", "rename" JSON_MAP_T m; JSON_MAP_SET(m, old_name, string); @@ -206,27 +208,27 @@ _publish_message("file-remove", json_util::from_map(m)); } -void octave_json_link::do_file_renamed (bool status) { +void octave_json_link::file_renamed (bool status) { // Called by "unlink", "rmdir", "rename" _publish_message("file-renamed", json_util::from_boolean(status)); } -void octave_json_link::do_execute_command_in_terminal(const std::string& command) { +void octave_json_link::execute_command_in_terminal(const std::string& command) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; JSON_MAP_SET(m, command, string); _publish_message("execute-command-in-terminal", json_util::from_map(m)); } -uint8NDArray octave_json_link::do_get_named_icon (const std::string& /* icon_name */) { +uint8NDArray octave_json_link::get_named_icon (const std::string& /* icon_name */) { // Called from msgbox.m // TODO: Implement request/response for this event uint8NDArray retval; return retval; } -void octave_json_link::do_set_workspace(bool top_level, bool debug, - const octave::symbol_info_list& ws, +void octave_json_link::set_workspace(bool top_level, bool debug, + const symbol_info_list& ws, bool update_variable_editor) { // Triggered on every new line entry JSON_MAP_T m; @@ -237,62 +239,63 @@ _publish_message("set-workspace", json_util::from_map(m)); } -void octave_json_link::do_clear_workspace(void) { +void octave_json_link::clear_workspace(void) { // Triggered on "clear" command (but not "clear all" or "clear foo") _publish_message("clear-workspace", json_util::empty()); } -void octave_json_link::do_set_history(const string_vector& hist) { +void octave_json_link::set_history(const string_vector& hist) { // Called at startup, possibly more? JSON_MAP_T m; JSON_MAP_SET(m, hist, string_vector); _publish_message("set-history", json_util::from_map(m)); } -void octave_json_link::do_append_history(const std::string& hist_entry) { +void octave_json_link::append_history(const std::string& hist_entry) { // Appears to be tied to readline, if available JSON_MAP_T m; JSON_MAP_SET(m, hist_entry, string); _publish_message("append-history", json_util::from_map(m)); } -void octave_json_link::do_clear_history(void) { +void octave_json_link::clear_history(void) { // Appears to be tied to readline, if available _publish_message("clear-history", json_util::empty()); } -void octave_json_link::do_clear_screen(void) { +void octave_json_link::clear_screen(void) { // Triggered by clc _publish_message("clear-screen", json_util::empty()); } -void octave_json_link::do_pre_input_event(void) { +void octave_json_link::pre_input_event(void) { // noop } -void octave_json_link::do_post_input_event(void) { +void octave_json_link::post_input_event(void) { // noop } -void octave_json_link::do_enter_debugger_event(const std::string& file, int line) { +void octave_json_link::enter_debugger_event(const std::string& fcn_name, const std::string& fcn_file_name, int line) { JSON_MAP_T m; - JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, fcn_name, string); + JSON_MAP_SET(m, fcn_file_name, string); JSON_MAP_SET(m, line, int); _publish_message("enter-debugger-event", json_util::from_map(m)); } -void octave_json_link::do_execute_in_debugger_event(const std::string& file, int line) { +void octave_json_link::execute_in_debugger_event(const std::string& file, int line) { JSON_MAP_T m; JSON_MAP_SET(m, file, string); JSON_MAP_SET(m, line, int); _publish_message("execute-in-debugger-event", json_util::from_map(m)); } -void octave_json_link::do_exit_debugger_event(void) { +void octave_json_link::exit_debugger_event(void) { _publish_message("exit-debugger-event", json_util::empty()); } -void octave_json_link::do_update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { +void octave_json_link::update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { JSON_MAP_T m; JSON_MAP_SET(m, insert, boolean); JSON_MAP_SET(m, file, string); @@ -310,34 +313,34 @@ // _publish_message("set-default-prompts", json_util::from_map(m)); // } -void octave_json_link::do_show_preferences(void) { +void octave_json_link::show_preferences(void) { // Triggered on "preferences" command _publish_message("show-preferences", json_util::empty()); } -std::string octave_json_link::do_gui_preference (const std::string& /* key */, const std::string& /* value */) { +std::string octave_json_link::gui_preference (const std::string& /* key */, const std::string& /* value */) { // Used by Octave GUI? // TODO: Implement request/response for this event std::string retval; return retval; } -void octave_json_link::do_show_doc(const std::string& file) { +void octave_json_link::show_doc(const std::string& file) { // Triggered on "doc" command _publish_message("show-doc", json_util::from_string(file)); } -void octave_json_link::do_register_doc (const std::string& file) { +void octave_json_link::register_doc (const std::string& file) { // Triggered by the GUI documentation viewer? _publish_message("register-doc", json_util::from_string(file)); } -void octave_json_link::do_unregister_doc (const std::string& file) { +void octave_json_link::unregister_doc (const std::string& file) { // Triggered by the GUI documentation viewer? _publish_message("unregister-doc", json_util::from_string(file)); } -void octave_json_link::do_edit_variable (const std::string& name, const octave_value& /* val */) { +void octave_json_link::edit_variable (const std::string& name, const octave_value& /* val */) { // Triggered on "openvar" command JSON_MAP_T m; JSON_MAP_SET(m, name, string); @@ -345,9 +348,9 @@ _publish_message("edit-variable", json_util::from_map(m)); } -void octave_json_link::do_show_static_plot(const std::string& term, const std::string& content) { +void octave_json_link::show_static_plot(const std::string& term, const std::string& content) { // Triggered on all plot commands with setenv("GNUTERM","svg") - int command_number = octave::command_editor::current_command_number(); + int command_number = command_editor::current_command_number(); JSON_MAP_T m; JSON_MAP_SET(m, term, string); JSON_MAP_SET(m, content, string); @@ -405,3 +408,4 @@ _json_main->publish_message(name, jobj); } +} // namespace octave diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/octave-json-link.h Fri Jul 03 03:58:38 2020 +0000 @@ -30,6 +30,10 @@ #include "json-util.h" #include "oct-mutex.h" +class string_vector; + +namespace octave { + // Circular reference class json_main; @@ -45,7 +49,7 @@ private: std::queue _queue; - octave::mutex _mutex; + mutex _mutex; }; class octave_json_link : public interpreter_events @@ -57,23 +61,23 @@ ~octave_json_link (void); - std::string do_request_input (const std::string& prompt) override; - std::string do_request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + std::string request_input (const std::string& prompt) override; + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; - bool do_confirm_shutdown (void) override; + bool confirm_shutdown (void) override; - bool do_copy_image_to_clipboard (const std::string& file) override; + bool copy_image_to_clipboard (const std::string& file) override; - bool do_edit_file (const std::string& file) override; - bool do_prompt_new_edit_file (const std::string& file) override; + bool edit_file (const std::string& file) override; + bool prompt_new_edit_file (const std::string& file) override; std::string - do_question_dialog (const std::string& msg, const std::string& title, + question_dialog (const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) override; std::pair, int> - do_list_dialog (const std::list& list, + list_dialog (const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, @@ -83,67 +87,67 @@ const std::string& cancel_string) override; std::list - do_input_dialog (const std::list& prompt, + input_dialog (const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) override; std::list - do_file_dialog (const filter_list& filter, const std::string& title, + file_dialog (const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) override; int - do_debug_cd_or_addpath_error (const std::string& file, + debug_cd_or_addpath_error (const std::string& file, const std::string& dir, bool addpath_option) override; - void do_change_directory (const std::string& dir) override; + void directory_changed (const std::string& dir) override; - void do_file_remove (const std::string& old_name, const std::string& new_name) override; - void do_file_renamed (bool) override; + void file_remove (const std::string& old_name, const std::string& new_name) override; + void file_renamed (bool) override; - void do_execute_command_in_terminal (const std::string& command) override; + void execute_command_in_terminal (const std::string& command) override; - uint8NDArray do_get_named_icon (const std::string& icon_name) override; + uint8NDArray get_named_icon (const std::string& icon_name) override; - void do_set_workspace (bool top_level, bool debug, - const octave::symbol_info_list& ws, + void set_workspace (bool top_level, bool debug, + const symbol_info_list& ws, bool update_variable_editor) override; - void do_clear_workspace (void) override; + void clear_workspace (void) override; - void do_set_history (const string_vector& hist) override; - void do_append_history (const std::string& hist_entry) override; - void do_clear_history (void) override; + void set_history (const string_vector& hist) override; + void append_history (const std::string& hist_entry) override; + void clear_history (void) override; - void do_clear_screen (void) override; + void clear_screen (void) override; - void do_pre_input_event (void) override; - void do_post_input_event (void) override; + void pre_input_event (void) override; + void post_input_event (void) override; - void do_enter_debugger_event (const std::string& file, int line) override; - void do_execute_in_debugger_event (const std::string& file, int line) override; - void do_exit_debugger_event (void) override; + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + void execute_in_debugger_event (const std::string& file, int line) override; + void exit_debugger_event (void) override; - void do_update_breakpoint (bool insert, + void update_breakpoint (bool insert, const std::string& file, int line, const std::string& cond) override; - void do_show_preferences (void) override; + void show_preferences (void) override; - std::string do_gui_preference (const std::string& key, const std::string& value) override; + std::string gui_preference (const std::string& key, const std::string& value) override; - void do_show_doc (const std::string& file) override; + void show_doc (const std::string& file) override; - void do_register_doc (const std::string& file) override; + void register_doc (const std::string& file) override; - void do_unregister_doc (const std::string& file) override; + void unregister_doc (const std::string& file) override; - void do_edit_variable (const std::string& name, const octave_value& val) override; + void edit_variable (const std::string& name, const octave_value& val) override; - void do_show_static_plot (const std::string& term, + void show_static_plot (const std::string& term, const std::string& content) override; // Custom methods @@ -207,4 +211,6 @@ return retval; } +} // namespace octave + #endif diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/corefcn/sysdep.cc Fri Jul 03 03:58:38 2020 +0000 @@ -755,7 +755,8 @@ { bool skip_redisplay = true; - octave_link::clear_screen (); + octave::event_manager& evmgr = octave::__get_event_manager__ ("clc"); + evmgr.clear_screen(); octave::command_editor::clear_screen (skip_redisplay); diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/octave.cc --- a/libinterp/octave.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/octave.cc Fri Jul 03 03:58:38 2020 +0000 @@ -382,6 +382,14 @@ sysdep_init (); } + bool application::link_enabled (void) const + { + if (m_interpreter) { + event_manager& evmgr = m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; + } + int cli_application::execute (void) { interpreter& interp = create_interpreter (); diff -r d78448f9c483 -r 97f7d1f4fe83 libinterp/octave.h --- a/libinterp/octave.h Thu Jul 02 18:08:38 2020 -0500 +++ b/libinterp/octave.h Fri Jul 03 03:58:38 2020 +0000 @@ -240,6 +240,7 @@ // both) of them... class interpreter; + class event_manager; // Base class for an Octave application. @@ -289,6 +290,8 @@ virtual bool gui_running (void) const { return false; } virtual void gui_running (bool) { } + bool link_enabled (void) const; + void program_invocation_name (const std::string& nm) { m_program_invocation_name = nm; } void program_name (const std::string& nm) { m_program_name = nm; } @@ -319,10 +322,7 @@ static bool is_link_enabled (void) { - if (instance && instance->m_interpreter) { - event_manager& evmgr = instance->m_interpreter->get_event_manager (); - return evmgr.enabled(); - } else return false; + return instance ? instance->link_enabled () : false; } // Convenience functions. diff -r d78448f9c483 -r 97f7d1f4fe83 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/liboctave/util/oct-mutex.cc Fri Jul 03 03:58:38 2020 +0000 @@ -180,7 +180,7 @@ void cond_wait (void) { - pthread_cond_wait (&condv, &pm); + pthread_cond_wait (&condv, &m_pm); } void cond_signal (void) diff -r d78448f9c483 -r 97f7d1f4fe83 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Thu Jul 02 18:08:38 2020 -0500 +++ b/liboctave/util/oct-mutex.h Fri Jul 03 03:58:38 2020 +0000 @@ -108,12 +108,12 @@ void cond_wait (void) { - rep->cond_wait (); + m_rep->cond_wait (); } void cond_signal (void) { - rep->cond_signal (); + m_rep->cond_signal (); } protected: diff -r d78448f9c483 -r 97f7d1f4fe83 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Thu Jul 02 18:08:38 2020 -0500 +++ b/liboctave/util/url-transfer.cc Fri Jul 03 03:58:38 2020 +0000 @@ -36,7 +36,6 @@ #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" -#include "libinterp/corefcn/interpreter-private.h" #include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" @@ -50,6 +49,10 @@ namespace octave { + // Forward declaration for event_manager + extern bool __event_manager_request_input_enabled__(); + extern std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success); + base_url_transfer::base_url_transfer (void) : m_host_or_url (), m_valid (false), m_ftp (false), m_ascii_mode (false), m_ok (true), m_errmsg (), @@ -249,38 +252,38 @@ link_transfer (void) : base_url_transfer () { - valid = true; + m_valid = true; } link_transfer (const std::string& host, const std::string& user_arg, const std::string& passwd, std::ostream& os) : base_url_transfer (host, user_arg, passwd, os) { - valid = true; + m_valid = true; // url = "ftp://" + host; } link_transfer (const std::string& url_str, std::ostream& os) : base_url_transfer (url_str, os) { - valid = true; + m_valid = true; } ~link_transfer (void) {} void http_get (const Array& param) { - perform (param, "get"); + perform_action (param, "get"); } void http_post (const Array& param) { - perform (param, "post"); + perform_action (param, "post"); } void http_action (const Array& param, const std::string& action) { - perform (param, action); + perform_action (param, action); } private: - void perform(const Array& param, const std::string& action) { - std::string url = host_or_url; + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; // Convert from Array to std::list std::list paramList; @@ -289,19 +292,18 @@ paramList.push_back(value); } - event_manager& evmgr = __get_event_manager__ ("link_transfer"); - if (evmgr.request_input_enabled ()) { + if (__event_manager_request_input_enabled__()) { bool success; - std::string result = evmgr.request_url (url, paramList, action, success); + std::string result = __event_manager_request_url__(url, paramList, action, success); if (success) { process_success(result); } else { - ok = false; - errmsg = result; + m_ok = false; + m_errmsg = result; } } else { - ok = false; - errmsg = "octave_link not connected for link_transfer"; + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; } } @@ -313,10 +315,10 @@ size_t outlen; bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); if (!b64_ok) { - ok = false; - errmsg = "failed decoding base64 from octave_link"; + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; } else { - curr_ostream->write(out, outlen); + m_curr_ostream->write(out, outlen); ::free(out); } } @@ -1025,8 +1027,7 @@ #endif url_transfer::url_transfer (void) { - event_manager& evmgr = __get_event_manager__ ("url_transfer"); - if (evmgr.request_input_enabled()) { + if (__event_manager_request_input_enabled__()) { m_rep.reset(new link_transfer()); } else { m_rep.reset(new REP_CLASS()); @@ -1035,8 +1036,7 @@ url_transfer::url_transfer (const std::string& host, const std::string& user, const std::string& passwd, std::ostream& os) { - event_manager& evmgr = __get_event_manager__ ("url_transfer"); - if (evmgr.request_input_enabled()) { + if (__event_manager_request_input_enabled__()) { m_rep.reset(new link_transfer(host, user, passwd, os)); } else { m_rep.reset(new REP_CLASS(host, user, passwd, os)); @@ -1044,8 +1044,7 @@ } url_transfer::url_transfer (const std::string& url, std::ostream& os) { - event_manager& evmgr = __get_event_manager__ ("url_transfer"); - if (evmgr.request_input_enabled()) { + if (__event_manager_request_input_enabled__()) { m_rep.reset(new link_transfer(url, os)); } else { m_rep.reset(new REP_CLASS(url, os)); ================================================ FILE: back-octave/oo-changesets/302-8900d7cf8554.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1593856818 18000 # Sat Jul 04 05:00:18 2020 -0500 # Branch oo-5.2a # Node ID 8900d7cf85541d1b927b7d071f541b69cd234f7c # Parent 97f7d1f4fe83bd6ebe102eb4cc6268ae23ad8d52 Another octave_link to event_manager rename diff -r 97f7d1f4fe83 -r 8900d7cf8554 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Fri Jul 03 03:58:38 2020 +0000 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Sat Jul 04 05:00:18 2020 -0500 @@ -60,8 +60,8 @@ should_plot_to_terminal = ( !strcmp (term, "dumb") && ( - __octave_link_plot_destination__ () == 0 || - __octave_link_plot_destination__ () == 2 + __event_manager_plot_destination__ () == 0 || + __event_manager_plot_destination__ () == 2 ) ); @@ -73,8 +73,8 @@ should_plot_to_temp_file = ( strcmp (term, "dumb") || - __octave_link_plot_destination__ () == 1 || - __octave_link_plot_destination__ () == 2 + __event_manager_plot_destination__ () == 1 || + __event_manager_plot_destination__ () == 2 ); if (should_plot_to_temp_file) @@ -101,7 +101,7 @@ if strcmp (term, "dumb") puts (a); else - __octave_link_show_static_plot__ (term, a); + __event_manager_show_static_plot__ (term, a); endif endif endif ================================================ FILE: back-octave/oo-changesets/310-1e1c91e6cddc.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1593894938 18000 # Sat Jul 04 15:35:38 2020 -0500 # Branch oo-6.0.1 # Node ID 1e1c91e6cddc3c48870e390b2730c7b586cc8a89 # Parent 171a2857d6d1cdcb77eef5cbef40e83962466d21 # Parent 8900d7cf85541d1b927b7d071f541b69cd234f7c Merge oo-5.2a into oo-6.0.1 diff -r 171a2857d6d1 -r 1e1c91e6cddc configure.ac --- a/configure.ac Sat Jul 04 11:13:35 2020 +0900 +++ b/configure.ac Sat Jul 04 15:35:38 2020 -0500 @@ -2812,7 +2812,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $SPARSE_XLDFLAGS $FFTW_XLDFLAGS $LLVM_LDFLAGS" diff -r 171a2857d6d1 -r 1e1c91e6cddc libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libgui/src/qt-interpreter-events.cc Sat Jul 04 15:35:38 2020 -0500 @@ -261,6 +261,21 @@ emit edit_variable_signal (QString::fromStdString (expr), val); } + void qt_interpreter_events::show_static_plot (const std::string&, const std::string&) + { + return; + } + + std::string qt_interpreter_events::request_input (const std::string&) + { + return {}; + } + + std::string qt_interpreter_events::request_url (const std::string&, const std::list&, const std::string&, bool&) + { + return {}; + } + bool qt_interpreter_events::confirm_shutdown (void) { QMutexLocker autolock (&m_mutex); @@ -505,6 +520,9 @@ emit clear_history_signal (); } + void qt_interpreter_events::do_clear_screen (void) + { } + void qt_interpreter_events::pre_input_event (void) { } diff -r 171a2857d6d1 -r 1e1c91e6cddc libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h Sat Jul 04 11:13:35 2020 +0900 +++ b/libgui/src/qt-interpreter-events.h Sat Jul 04 15:35:38 2020 -0500 @@ -118,6 +118,12 @@ void edit_variable (const std::string& name, const octave_value& val); + void show_static_plot (const std::string& term, const std::string& content); + + std::string request_input (const std::string&); + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool confirm_shutdown (void); bool prompt_new_edit_file (const std::string& file); @@ -160,6 +166,8 @@ void clear_history (void); + void clear_screen (void); + void pre_input_event (void); void post_input_event (void); diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/event-manager.cc Sat Jul 04 15:35:38 2020 -0500 @@ -41,6 +41,17 @@ namespace octave { + + bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ ("request_input_enabled"); + return evmgr.request_input_enabled(); + } + + std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ ("request_url"); + return evmgr.request_url(url, param, action, success); + } + static int readline_event_hook (void) { event_manager& evmgr = __get_event_manager__ ("octave_readline_hook"); @@ -652,3 +663,28 @@ evmgr.focus_window ("workspace"); return ovl (); } + +DEFMETHOD (__event_manager_plot_destination__, interp, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (interp.get_event_manager().plot_destination()); +} + +DEFMETHOD (__event_manager_show_static_plot__, interp, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (interp.get_event_manager().show_static_plot(term, content)); +} + diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/event-manager.h Sat Jul 04 15:35:38 2020 -0500 @@ -48,6 +48,12 @@ class symbol_info_list; + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + // The methods in this class provide a way to pass signals to the GUI // thread. A GUI that wishes to act on these events should derive // from this class and perform actions in a thread-safe way. In @@ -147,6 +153,13 @@ // confirmation before another action. Could these be reformulated // using the question_dialog action? + bool _request_input_enabled; + virtual std::string request_input (const std::string&) = 0; + virtual std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + + plot_destination_t _plot_destination; + virtual void show_static_plot (const std::string& term, const std::string& content) = 0; + virtual bool confirm_shutdown (void) { return false; } virtual bool prompt_new_edit_file (const std::string& /*file*/) @@ -220,6 +233,8 @@ virtual void clear_history (void) { } + virtual void clear_screen (void) { } + virtual void pre_input_event (void) { } virtual void post_input_event (void) { } @@ -373,6 +388,28 @@ instance->update_path_dialog (); } + bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + + plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->show_static_plot (term, content); + return true; + } + else + return false; + } + bool show_preferences (void) { if (enabled ()) @@ -551,6 +588,12 @@ instance->clear_history (); } + void clear_screen (void) + { + if (enabled ()) + instance->clear_screen (); + } + void pre_input_event (void) { if (enabled ()) @@ -563,6 +606,21 @@ instance->post_input_event (); } + + std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->request_input (prompt) + : std::string (); + } + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->request_url (url, param, action, success) + : std::string (); + } + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) { diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/input.cc Sat Jul 04 15:35:38 2020 -0500 @@ -671,7 +671,12 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + event_manager& evmgr = m_interpreter.get_event_manager (); + if (evmgr.request_input_enabled ()) + retval = evmgr.request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1443,3 +1448,32 @@ } // #endif + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/interpreter.cc Sat Jul 04 15:35:38 2020 -0500 @@ -59,6 +59,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave.h" @@ -600,6 +601,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (*this, options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } // FIXME: we defer creation of the gh_manager object because it diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Sat Jul 04 15:35:38 2020 -0500 @@ -0,0 +1,101 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" +#include "interpreter.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +namespace octave { + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (new octave_json_link(this)) +{ + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + event_manager& evmgr = interp.get_event_manager (); + evmgr.connect_link (_octave_json_link); + evmgr.enable (); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link->receive_message(name, jobj); +} + +} // namespace octave diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Sat Jul 04 15:35:38 2020 -0500 @@ -0,0 +1,37 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +namespace octave { + +class interpreter; + +class json_main { +public: + json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + + std::shared_ptr _octave_json_link; +}; + +} // namespace octave + +#endif diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Sat Jul 04 15:35:38 2020 -0500 @@ -0,0 +1,264 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +namespace octave { + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_symbol_info_list(const symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_filter_list(const interpreter_events::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_symbol_info(const symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + + JSON_MAP_T m; + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (size_t i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} + +} // namespace octave diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Sat Jul 04 15:35:38 2020 -0500 @@ -0,0 +1,64 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "syminfo.h" +#include "event-manager.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +namespace octave { + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const interpreter_events::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_symbol_info(const symbol_info element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +} // namespace octave + +#endif diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/module.mk Sat Jul 04 15:35:38 2020 -0500 @@ -45,6 +45,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/load-path.h \ %reldir%/load-save.h \ @@ -73,6 +75,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ %reldir%/pr-output.h \ @@ -182,6 +185,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ %reldir%/inv.cc \ @@ -218,6 +223,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ %reldir%/pinv.cc \ diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Sat Jul 04 15:35:38 2020 -0500 @@ -0,0 +1,411 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +namespace octave { + +octave_json_link::octave_json_link(json_main* __json_main) + : interpreter_events (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); + +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); + +// return true; +// } + +bool octave_json_link::copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); + +// return message_dialog_queue.dequeue(); +// } + +std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::directory_changed(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + +void octave_json_link::execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +uint8NDArray octave_json_link::get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::set_workspace(bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::pre_input_event(void) { + // noop +} + +void octave_json_link::post_input_event(void) { + // noop +} + +void octave_json_link::enter_debugger_event(const std::string& fcn_name, const std::string& fcn_file_name, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, fcn_name, string); + JSON_MAP_SET(m, fcn_file_name, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } + +void octave_json_link::show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +std::string octave_json_link::gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + +void octave_json_link::show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +void octave_json_link::register_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::unregister_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} + +void octave_json_link::show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + +} // namespace octave diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Sat Jul 04 15:35:38 2020 -0500 @@ -0,0 +1,216 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "event-manager.h" +#include "json-util.h" +#include "oct-mutex.h" + +class string_vector; + +namespace octave { + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + mutex _mutex; +}; + +class octave_json_link : public interpreter_events +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string request_input (const std::string& prompt) override; + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + bool confirm_shutdown (void) override; + + bool copy_image_to_clipboard (const std::string& file) override; + + bool edit_file (const std::string& file) override; + bool prompt_new_edit_file (const std::string& file) override; + + std::string + question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + int + debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + void directory_changed (const std::string& dir) override; + + void file_remove (const std::string& old_name, const std::string& new_name) override; + void file_renamed (bool) override; + + void execute_command_in_terminal (const std::string& command) override; + + uint8NDArray get_named_icon (const std::string& icon_name) override; + + void set_workspace (bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) override; + + void clear_workspace (void) override; + + void set_history (const string_vector& hist) override; + void append_history (const std::string& hist_entry) override; + void clear_history (void) override; + + void clear_screen (void) override; + + void pre_input_event (void) override; + void post_input_event (void) override; + + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + void execute_in_debugger_event (const std::string& file, int line) override; + void exit_debugger_event (void) override; + + void update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + void show_preferences (void) override; + + std::string gui_preference (const std::string& key, const std::string& value) override; + + void show_doc (const std::string& file) override; + + void register_doc (const std::string& file) override; + + void unregister_doc (const std::string& file) override; + + void edit_variable (const std::string& name, const octave_value& val) override; + + void show_static_plot (const std::string& term, + const std::string& content) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +} // namespace octave + +#endif diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/syscalls.cc Sat Jul 04 15:35:38 2020 -0500 @@ -149,9 +149,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -461,42 +460,6 @@ return ovl (status, msg); } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (interp.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = octave::sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/sysdep.cc Sat Jul 04 15:35:38 2020 -0500 @@ -75,6 +75,7 @@ #include "defun.h" #include "error.h" #include "errwarn.h" +#include "event-manager.h" #include "input.h" #include "interpreter-private.h" #include "octave.h" @@ -651,7 +652,7 @@ // Read one character from the terminal. - int kbhit (bool wait) + int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -678,13 +679,24 @@ set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + event_manager& evmgr = __get_event_manager__ ("kbhit"); + if (evmgr.request_input_enabled ()) { + std::string line = evmgr.request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). set_interrupt_handler (saved_interrupt_handler, true); @@ -743,6 +755,9 @@ { bool skip_redisplay = true; + octave::event_manager& evmgr = octave::__get_event_manager__ ("clc"); + evmgr.clear_screen(); + octave::command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1169,7 +1184,7 @@ Fdrawnow (interp); - int c = octave::kbhit (args.length () == 0); + int c = octave::kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/sysdep.h Sat Jul 04 15:35:38 2020 -0500 @@ -49,7 +49,7 @@ extern OCTINTERP_API int pclose (FILE *f); - extern OCTINTERP_API int kbhit (bool wait = true); + extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); @@ -107,7 +107,7 @@ inline int octave_kbhit (bool wait = true) { - return octave::kbhit (wait); + return octave::kbhit ("", wait); } OCTAVE_DEPRECATED (5, "use 'octave::get_P_tmpdir' instead") diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/corefcn/utils.cc Sat Jul 04 15:35:38 2020 -0500 @@ -1442,7 +1442,7 @@ if (do_graphics_events) gh_mgr.process_events (); - c = kbhit (false); + c = kbhit ("press enter to continue", false); } } else diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/octave.cc --- a/libinterp/octave.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/octave.cc Sat Jul 04 15:35:38 2020 -0500 @@ -187,6 +187,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -372,6 +382,14 @@ sysdep_init (); } + bool application::link_enabled (void) const + { + if (m_interpreter) { + event_manager& evmgr = m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; + } + int cli_application::execute (void) { interpreter& interp = create_interpreter (); @@ -397,7 +415,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (octave::application::is_gui_running ()); + return ovl (octave::application::is_link_enabled ()); } /* diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/octave.h --- a/libinterp/octave.h Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/octave.h Sat Jul 04 15:35:38 2020 -0500 @@ -79,6 +79,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -109,6 +111,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -215,6 +219,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; @@ -228,6 +240,7 @@ // both) of them... class interpreter; + class event_manager; // Base class for an Octave application. @@ -277,6 +290,8 @@ virtual bool gui_running (void) const { return false; } virtual void gui_running (bool) { } + bool link_enabled (void) const; + void program_invocation_name (const std::string& nm) { m_program_invocation_name = nm; } void program_name (const std::string& nm) { m_program_name = nm; } @@ -305,6 +320,11 @@ return instance ? instance->gui_running () : false; } + static bool is_link_enabled (void) + { + return instance ? instance->link_enabled () : false; + } + // Convenience functions. static bool forced_interactive (void); diff -r 171a2857d6d1 -r 1e1c91e6cddc libinterp/options-usage.h --- a/libinterp/options-usage.h Sat Jul 04 11:13:35 2020 +0900 +++ b/libinterp/options-usage.h Sat Jul 04 15:35:38 2020 -0500 @@ -38,10 +38,10 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--line-editing] [--no-gui] [--no-history]\n\ - [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--silent] [--traditional]\n\ + [--jit-compiler] [--json-sock] [--json-max-len] [--line-editing]\n\ + [--no-gui] [--no-history][--no-init-file] [--no-init-path]\n\ + [--no-line-editing] [--no-site-file] [--no-window-system] [--norc]\n\ + [-p path] [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; // This is here so that it's more likely that the usage message and @@ -68,15 +68,17 @@ #define INFO_PROG_OPTION 8 #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 -#define LINE_EDITING_OPTION 11 -#define NO_GUI_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define TEXI_MACROS_FILE_OPTION 18 -#define TRADITIONAL_OPTION 19 +#define JSON_SOCK_OPTION 11 +#define JSON_MAX_LEN_OPTION 12 +#define LINE_EDITING_OPTION 13 +#define NO_GUI_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define TEXI_MACROS_FILE_OPTION 20 +#define TRADITIONAL_OPTION 21 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, 0, TRADITIONAL_OPTION }, @@ -94,6 +96,8 @@ { "info-program", octave_required_arg, 0, INFO_PROG_OPTION }, { "interactive", octave_no_arg, 0, 'i' }, { "jit-compiler", octave_no_arg, 0, JIT_COMPILER_OPTION }, + { "json-sock", octave_required_arg, 0, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, 0, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, 0, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, 0, NO_GUI_OPTION }, { "no-history", octave_no_arg, 0, 'H' }, @@ -145,6 +149,8 @@ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 171a2857d6d1 -r 1e1c91e6cddc liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/liboctave/util/oct-mutex.cc Sat Jul 04 15:35:38 2020 -0500 @@ -58,6 +58,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -68,11 +80,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -90,8 +104,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -123,11 +148,19 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&m_pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&m_pm); + pthread_mutex_destroy (&m_pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -145,8 +178,19 @@ return (pthread_mutex_trylock (&m_pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &m_pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t m_pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 171a2857d6d1 -r 1e1c91e6cddc liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Sat Jul 04 11:13:35 2020 +0900 +++ b/liboctave/util/oct-mutex.h Sat Jul 04 15:35:38 2020 -0500 @@ -50,6 +50,10 @@ virtual bool try_lock (void); + virtual void cond_wait (void); + + virtual void cond_signal (void); + private: refcount m_count; }; @@ -102,6 +106,16 @@ return m_rep->try_lock (); } + void cond_wait (void) + { + m_rep->cond_wait (); + } + + void cond_signal (void) + { + m_rep->cond_signal (); + } + protected: base_mutex *m_rep; }; diff -r 171a2857d6d1 -r 1e1c91e6cddc liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sat Jul 04 11:13:35 2020 +0900 +++ b/liboctave/util/url-transfer.cc Sat Jul 04 15:35:38 2020 -0500 @@ -36,6 +36,7 @@ #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" +#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" #include "version.h" @@ -48,6 +49,10 @@ namespace octave { + // Forward declaration for event_manager + extern bool __event_manager_request_input_enabled__(); + extern std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success); + base_url_transfer::base_url_transfer (void) : m_host_or_url (), m_valid (false), m_ftp (false), m_ascii_mode (false), m_ok (true), m_errmsg (), @@ -240,6 +245,86 @@ return file_list; } + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + m_valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + m_valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + m_valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform_action (param, "get"); + } + + void http_post (const Array& param) { + perform_action (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform_action (param, action); + } + +private: + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (__event_manager_request_input_enabled__()) { + bool success; + std::string result = __event_manager_request_url__(url, paramList, action, success); + if (success) { + process_success(result); + } else { + m_ok = false; + m_errmsg = result; + } + } else { + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; + } else { + m_curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -941,17 +1026,30 @@ # define REP_CLASS base_url_transfer #endif - url_transfer::url_transfer (void) : m_rep (new REP_CLASS ()) - { } + url_transfer::url_transfer (void) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new REP_CLASS()); + } + } url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : m_rep (new REP_CLASS (host, user, passwd, os)) - { } + const std::string& passwd, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new REP_CLASS(host, user, passwd, os)); + } + } - url_transfer::url_transfer (const std::string& url, std::ostream& os) - : m_rep (new REP_CLASS (url, os)) - { } + url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new REP_CLASS(url, os)); + } + } #undef REP_CLASS diff -r 171a2857d6d1 -r 1e1c91e6cddc scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Sat Jul 04 11:13:35 2020 +0900 +++ b/scripts/help/__unimplemented__.m Sat Jul 04 15:35:38 2020 -0500 @@ -45,7 +45,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -524,6 +547,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -566,13 +590,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run 'pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 171a2857d6d1 -r 1e1c91e6cddc scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Sat Jul 04 11:13:35 2020 +0900 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Sat Jul 04 15:35:38 2020 -0500 @@ -32,9 +32,84 @@ if (nargin < 1 || nargin > 4 || nargin == 2) print_usage (); + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __event_manager_plot_destination__ () == 0 || + __event_manager_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif + + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __event_manager_plot_destination__ () == 1 || + __event_manager_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __event_manager_show_static_plot__ (term, a); + endif + endif + endif + endif +endfunction - if (nargin >= 3 && nargin <= 4) +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -70,44 +145,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/320-8d4683a83238.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640589439 21600 # Mon Dec 27 01:17:19 2021 -0600 # Branch oo-6.4.0 # Node ID 8d4683a83238d9f41c16f6b9138c72530f0cc9a6 # Parent 8d7671609955afabf79ceff678cc41eea61583f2 # Parent 1e1c91e6cddc3c48870e390b2730c7b586cc8a89 Merge oo-6.0.1 into oo-6.4.0 diff -r 8d7671609955 -r 8d4683a83238 configure.ac --- a/configure.ac Sat Oct 30 10:20:24 2021 -0400 +++ b/configure.ac Mon Dec 27 01:17:19 2021 -0600 @@ -2833,7 +2833,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $LLVM_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $SPARSE_XLDFLAGS $FFTW_XLDFLAGS $LLVM_LDFLAGS" diff -r 8d7671609955 -r 8d4683a83238 libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libgui/src/qt-interpreter-events.cc Mon Dec 27 01:17:19 2021 -0600 @@ -264,6 +264,21 @@ emit edit_variable_signal (QString::fromStdString (expr), val); } + void qt_interpreter_events::show_static_plot (const std::string&, const std::string&) + { + return; + } + + std::string qt_interpreter_events::request_input (const std::string&) + { + return {}; + } + + std::string qt_interpreter_events::request_url (const std::string&, const std::list&, const std::string&, bool&) + { + return {}; + } + bool qt_interpreter_events::confirm_shutdown (void) { QMutexLocker autolock (&m_mutex); @@ -508,6 +523,9 @@ emit clear_history_signal (); } + void qt_interpreter_events::do_clear_screen (void) + { } + void qt_interpreter_events::pre_input_event (void) { } diff -r 8d7671609955 -r 8d4683a83238 libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h Sat Oct 30 10:20:24 2021 -0400 +++ b/libgui/src/qt-interpreter-events.h Mon Dec 27 01:17:19 2021 -0600 @@ -118,6 +118,12 @@ void edit_variable (const std::string& name, const octave_value& val); + void show_static_plot (const std::string& term, const std::string& content); + + std::string request_input (const std::string&); + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool confirm_shutdown (void); bool prompt_new_edit_file (const std::string& file); @@ -160,6 +166,8 @@ void clear_history (void); + void clear_screen (void); + void pre_input_event (void); void post_input_event (void); diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/event-manager.cc Mon Dec 27 01:17:19 2021 -0600 @@ -41,6 +41,17 @@ namespace octave { + + bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ ("request_input_enabled"); + return evmgr.request_input_enabled(); + } + + std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ ("request_url"); + return evmgr.request_url(url, param, action, success); + } + static int readline_event_hook (void) { event_manager& evmgr = __get_event_manager__ ("octave_readline_hook"); @@ -652,3 +663,28 @@ evmgr.focus_window ("workspace"); return ovl (); } + +DEFMETHOD (__event_manager_plot_destination__, interp, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (interp.get_event_manager().plot_destination()); +} + +DEFMETHOD (__event_manager_show_static_plot__, interp, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (interp.get_event_manager().show_static_plot(term, content)); +} + diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/event-manager.h Mon Dec 27 01:17:19 2021 -0600 @@ -48,6 +48,12 @@ class symbol_info_list; + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + // The methods in this class provide a way to pass signals to the GUI // thread. A GUI that wishes to act on these events should derive // from this class and perform actions in a thread-safe way. In @@ -147,6 +153,13 @@ // confirmation before another action. Could these be reformulated // using the question_dialog action? + bool _request_input_enabled; + virtual std::string request_input (const std::string&) = 0; + virtual std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + + plot_destination_t _plot_destination; + virtual void show_static_plot (const std::string& term, const std::string& content) = 0; + virtual bool confirm_shutdown (void) { return false; } virtual bool prompt_new_edit_file (const std::string& /*file*/) @@ -220,6 +233,8 @@ virtual void clear_history (void) { } + virtual void clear_screen (void) { } + virtual void pre_input_event (void) { } virtual void post_input_event (void) { } @@ -373,6 +388,28 @@ instance->update_path_dialog (); } + bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + + plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->show_static_plot (term, content); + return true; + } + else + return false; + } + bool show_preferences (void) { if (enabled ()) @@ -551,6 +588,12 @@ instance->clear_history (); } + void clear_screen (void) + { + if (enabled ()) + instance->clear_screen (); + } + void pre_input_event (void) { if (enabled ()) @@ -563,6 +606,21 @@ instance->post_input_event (); } + + std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->request_input (prompt) + : std::string (); + } + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->request_url (url, param, action, success) + : std::string (); + } + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) { diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/input.cc Mon Dec 27 01:17:19 2021 -0600 @@ -699,7 +699,12 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + event_manager& evmgr = m_interpreter.get_event_manager (); + if (evmgr.request_input_enabled ()) + retval = evmgr.request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1525,3 +1530,32 @@ } // #endif + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/interpreter.cc Mon Dec 27 01:17:19 2021 -0600 @@ -59,6 +59,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave.h" @@ -599,6 +600,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (*this, options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } // FIXME: we defer creation of the gh_manager object because it diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Mon Dec 27 01:17:19 2021 -0600 @@ -0,0 +1,101 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" +#include "interpreter.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +namespace octave { + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (new octave_json_link(this)) +{ + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + event_manager& evmgr = interp.get_event_manager (); + evmgr.connect_link (_octave_json_link); + evmgr.enable (); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link->receive_message(name, jobj); +} + +} // namespace octave diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Mon Dec 27 01:17:19 2021 -0600 @@ -0,0 +1,37 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +namespace octave { + +class interpreter; + +class json_main { +public: + json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + + std::shared_ptr _octave_json_link; +}; + +} // namespace octave + +#endif diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Mon Dec 27 01:17:19 2021 -0600 @@ -0,0 +1,264 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +namespace octave { + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_symbol_info_list(const symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_filter_list(const interpreter_events::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_symbol_info(const symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + + JSON_MAP_T m; + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (size_t i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} + +} // namespace octave diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Mon Dec 27 01:17:19 2021 -0600 @@ -0,0 +1,64 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "syminfo.h" +#include "event-manager.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +namespace octave { + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const interpreter_events::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_symbol_info(const symbol_info element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +} // namespace octave + +#endif diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/module.mk Mon Dec 27 01:17:19 2021 -0600 @@ -45,6 +45,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/load-path.h \ %reldir%/load-save.h \ @@ -73,6 +75,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ %reldir%/pr-output.h \ @@ -182,6 +185,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ %reldir%/inv.cc \ @@ -218,6 +223,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ %reldir%/pinv.cc \ diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Mon Dec 27 01:17:19 2021 -0600 @@ -0,0 +1,411 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +namespace octave { + +octave_json_link::octave_json_link(json_main* __json_main) + : interpreter_events (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); + +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); + +// return true; +// } + +bool octave_json_link::copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); + +// return message_dialog_queue.dequeue(); +// } + +std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::directory_changed(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + +void octave_json_link::execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +uint8NDArray octave_json_link::get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::set_workspace(bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::pre_input_event(void) { + // noop +} + +void octave_json_link::post_input_event(void) { + // noop +} + +void octave_json_link::enter_debugger_event(const std::string& fcn_name, const std::string& fcn_file_name, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, fcn_name, string); + JSON_MAP_SET(m, fcn_file_name, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } + +void octave_json_link::show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +std::string octave_json_link::gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + +void octave_json_link::show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +void octave_json_link::register_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::unregister_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} + +void octave_json_link::show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + +} // namespace octave diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Mon Dec 27 01:17:19 2021 -0600 @@ -0,0 +1,216 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "event-manager.h" +#include "json-util.h" +#include "oct-mutex.h" + +class string_vector; + +namespace octave { + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + mutex _mutex; +}; + +class octave_json_link : public interpreter_events +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string request_input (const std::string& prompt) override; + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + bool confirm_shutdown (void) override; + + bool copy_image_to_clipboard (const std::string& file) override; + + bool edit_file (const std::string& file) override; + bool prompt_new_edit_file (const std::string& file) override; + + std::string + question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + int + debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + void directory_changed (const std::string& dir) override; + + void file_remove (const std::string& old_name, const std::string& new_name) override; + void file_renamed (bool) override; + + void execute_command_in_terminal (const std::string& command) override; + + uint8NDArray get_named_icon (const std::string& icon_name) override; + + void set_workspace (bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) override; + + void clear_workspace (void) override; + + void set_history (const string_vector& hist) override; + void append_history (const std::string& hist_entry) override; + void clear_history (void) override; + + void clear_screen (void) override; + + void pre_input_event (void) override; + void post_input_event (void) override; + + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + void execute_in_debugger_event (const std::string& file, int line) override; + void exit_debugger_event (void) override; + + void update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + void show_preferences (void) override; + + std::string gui_preference (const std::string& key, const std::string& value) override; + + void show_doc (const std::string& file) override; + + void register_doc (const std::string& file) override; + + void unregister_doc (const std::string& file) override; + + void edit_variable (const std::string& name, const octave_value& val) override; + + void show_static_plot (const std::string& term, + const std::string& content) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +} // namespace octave + +#endif diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/syscalls.cc Mon Dec 27 01:17:19 2021 -0600 @@ -149,9 +149,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -461,42 +460,6 @@ return ovl (status, msg); } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (interp.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = octave::sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/sysdep.cc Mon Dec 27 01:17:19 2021 -0600 @@ -75,6 +75,7 @@ #include "defun.h" #include "error.h" #include "errwarn.h" +#include "event-manager.h" #include "input.h" #include "interpreter-private.h" #include "octave.h" @@ -651,7 +652,7 @@ // Read one character from the terminal. - int kbhit (bool wait) + int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -678,13 +679,24 @@ set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + event_manager& evmgr = __get_event_manager__ ("kbhit"); + if (evmgr.request_input_enabled ()) { + std::string line = evmgr.request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). set_interrupt_handler (saved_interrupt_handler, true); @@ -743,6 +755,9 @@ { bool skip_redisplay = true; + octave::event_manager& evmgr = octave::__get_event_manager__ ("clc"); + evmgr.clear_screen(); + octave::command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1169,7 +1184,7 @@ Fdrawnow (interp); - int c = octave::kbhit (args.length () == 0); + int c = octave::kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/sysdep.h Mon Dec 27 01:17:19 2021 -0600 @@ -49,7 +49,7 @@ extern OCTINTERP_API int pclose (FILE *f); - extern OCTINTERP_API int kbhit (bool wait = true); + extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); @@ -107,7 +107,7 @@ inline int octave_kbhit (bool wait = true) { - return octave::kbhit (wait); + return octave::kbhit ("", wait); } OCTAVE_DEPRECATED (5, "use 'octave::get_P_tmpdir' instead") diff -r 8d7671609955 -r 8d4683a83238 libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/corefcn/utils.cc Mon Dec 27 01:17:19 2021 -0600 @@ -1442,7 +1442,7 @@ if (do_graphics_events) gh_mgr.process_events (); - c = kbhit (false); + c = kbhit ("press enter to continue", false); } } else diff -r 8d7671609955 -r 8d4683a83238 libinterp/octave.cc --- a/libinterp/octave.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/octave.cc Mon Dec 27 01:17:19 2021 -0600 @@ -188,6 +188,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -373,6 +383,14 @@ sysdep_init (); } + bool application::link_enabled (void) const + { + if (m_interpreter) { + event_manager& evmgr = m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; + } + int cli_application::execute (void) { interpreter& interp = create_interpreter (); @@ -396,7 +414,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (octave::application::is_gui_running ()); + return ovl (octave::application::is_link_enabled ()); } /* diff -r 8d7671609955 -r 8d4683a83238 libinterp/octave.h --- a/libinterp/octave.h Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/octave.h Mon Dec 27 01:17:19 2021 -0600 @@ -79,6 +79,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -109,6 +111,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -215,6 +219,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; @@ -228,6 +240,7 @@ // both) of them... class interpreter; + class event_manager; // Base class for an Octave application. @@ -277,6 +290,8 @@ virtual bool gui_running (void) const { return false; } virtual void gui_running (bool) { } + bool link_enabled (void) const; + void program_invocation_name (const std::string& nm) { m_program_invocation_name = nm; } void program_name (const std::string& nm) { m_program_name = nm; } @@ -305,6 +320,11 @@ return instance ? instance->gui_running () : false; } + static bool is_link_enabled (void) + { + return instance ? instance->link_enabled () : false; + } + // Convenience functions. static bool forced_interactive (void); diff -r 8d7671609955 -r 8d4683a83238 libinterp/options.h --- a/libinterp/options.h Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/options.h Mon Dec 27 01:17:19 2021 -0600 @@ -54,16 +54,18 @@ #define INFO_PROG_OPTION 8 #define DEBUG_JIT_OPTION 9 #define JIT_COMPILER_OPTION 10 -#define LINE_EDITING_OPTION 11 -#define NO_GUI_OPTION 12 -#define NO_GUI_LIBS_OPTION 13 -#define NO_INIT_FILE_OPTION 14 -#define NO_INIT_PATH_OPTION 15 -#define NO_LINE_EDITING_OPTION 16 -#define NO_SITE_FILE_OPTION 17 -#define PERSIST_OPTION 18 -#define TEXI_MACROS_FILE_OPTION 19 -#define TRADITIONAL_OPTION 20 +#define JSON_SOCK_OPTION 11 +#define JSON_MAX_LEN_OPTION 12 +#define LINE_EDITING_OPTION 13 +#define NO_GUI_OPTION 14 +#define NO_GUI_LIBS_OPTION 15 +#define NO_INIT_FILE_OPTION 16 +#define NO_INIT_PATH_OPTION 17 +#define NO_LINE_EDITING_OPTION 18 +#define NO_SITE_FILE_OPTION 19 +#define PERSIST_OPTION 20 +#define TEXI_MACROS_FILE_OPTION 21 +#define TRADITIONAL_OPTION 22 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, 0, TRADITIONAL_OPTION }, @@ -82,6 +84,8 @@ { "info-program", octave_required_arg, 0, INFO_PROG_OPTION }, { "interactive", octave_no_arg, 0, 'i' }, { "jit-compiler", octave_no_arg, 0, JIT_COMPILER_OPTION }, + { "json-sock", octave_required_arg, 0, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, 0, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, 0, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, 0, NO_GUI_OPTION }, { "no-gui-libs", octave_no_arg, 0, NO_GUI_LIBS_OPTION }, diff -r 8d7671609955 -r 8d4683a83238 libinterp/usage.h --- a/libinterp/usage.h Sat Oct 30 10:20:24 2021 -0400 +++ b/libinterp/usage.h Mon Dec 27 01:17:19 2021 -0600 @@ -38,10 +38,10 @@ [--echo-commands] [--eval CODE] [--exec-path path]\n\ [--gui] [--help] [--image-path path]\n\ [--info-file file] [--info-program prog] [--interactive]\n\ - [--jit-compiler] [--line-editing] [--no-gui] [--no-history]\n\ - [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--silent] [--traditional]\n\ + [--jit-compiler] [--json-sock] [--json-max-len] [--line-editing]\n\ + [--no-gui] [--no-history][--no-init-file] [--no-init-path]\n\ + [--no-line-editing] [--no-site-file] [--no-window-system] [--norc]\n\ + [-p path] [--path path] [--persist] [--silent] [--traditional]\n\ [--verbose] [--version] [file]"; // Usage message with extra help. @@ -70,6 +70,8 @@ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ --jit-compiler Enable the JIT compiler.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 8d7671609955 -r 8d4683a83238 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/liboctave/util/oct-mutex.cc Mon Dec 27 01:17:19 2021 -0600 @@ -58,6 +58,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -68,11 +80,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -90,8 +104,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -123,11 +148,19 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&m_pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&m_pm); + pthread_mutex_destroy (&m_pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -145,8 +178,19 @@ return (pthread_mutex_trylock (&m_pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &m_pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t m_pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 8d7671609955 -r 8d4683a83238 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Sat Oct 30 10:20:24 2021 -0400 +++ b/liboctave/util/oct-mutex.h Mon Dec 27 01:17:19 2021 -0600 @@ -50,6 +50,10 @@ virtual bool try_lock (void); + virtual void cond_wait (void); + + virtual void cond_signal (void); + private: refcount m_count; }; @@ -102,6 +106,16 @@ return m_rep->try_lock (); } + void cond_wait (void) + { + m_rep->cond_wait (); + } + + void cond_signal (void) + { + m_rep->cond_signal (); + } + protected: base_mutex *m_rep; }; diff -r 8d7671609955 -r 8d4683a83238 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sat Oct 30 10:20:24 2021 -0400 +++ b/liboctave/util/url-transfer.cc Mon Dec 27 01:17:19 2021 -0600 @@ -36,6 +36,7 @@ #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" +#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" #include "version.h" @@ -48,6 +49,10 @@ namespace octave { + // Forward declaration for event_manager + extern bool __event_manager_request_input_enabled__(); + extern std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success); + base_url_transfer::base_url_transfer (void) : m_host_or_url (), m_valid (false), m_ftp (false), m_ascii_mode (false), m_ok (true), m_errmsg (), @@ -240,6 +245,86 @@ return file_list; } + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + m_valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + m_valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + m_valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform_action (param, "get"); + } + + void http_post (const Array& param) { + perform_action (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform_action (param, action); + } + +private: + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (__event_manager_request_input_enabled__()) { + bool success; + std::string result = __event_manager_request_url__(url, paramList, action, success); + if (success) { + process_success(result); + } else { + m_ok = false; + m_errmsg = result; + } + } else { + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; + } else { + m_curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -941,17 +1026,30 @@ # define REP_CLASS base_url_transfer #endif - url_transfer::url_transfer (void) : m_rep (new REP_CLASS ()) - { } + url_transfer::url_transfer (void) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new REP_CLASS()); + } + } url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : m_rep (new REP_CLASS (host, user, passwd, os)) - { } + const std::string& passwd, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new REP_CLASS(host, user, passwd, os)); + } + } - url_transfer::url_transfer (const std::string& url, std::ostream& os) - : m_rep (new REP_CLASS (url, os)) - { } + url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new REP_CLASS(url, os)); + } + } #undef REP_CLASS diff -r 8d7671609955 -r 8d4683a83238 scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Sat Oct 30 10:20:24 2021 -0400 +++ b/scripts/help/__unimplemented__.m Mon Dec 27 01:17:19 2021 -0600 @@ -45,7 +45,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -524,6 +547,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -566,13 +590,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run 'pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 8d7671609955 -r 8d4683a83238 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Sat Oct 30 10:20:24 2021 -0400 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Mon Dec 27 01:17:19 2021 -0600 @@ -32,9 +32,84 @@ if (nargin < 1 || nargin > 4 || nargin == 2) print_usage (); - endif + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __event_manager_plot_destination__ () == 0 || + __event_manager_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif - if (nargin >= 3 && nargin <= 4) + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __event_manager_plot_destination__ () == 1 || + __event_manager_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __event_manager_show_static_plot__ (term, a); + endif + endif + endif + + endif +endfunction + +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -70,44 +145,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/321-faad58416a3a.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640591257 21600 # Mon Dec 27 01:47:37 2021 -0600 # Branch oo-6.4.0 # Node ID faad58416a3a44c493ee817964bd11407895a60f # Parent 8d4683a83238d9f41c16f6b9138c72530f0cc9a6 Remove gnuplot warning diff -r 8d4683a83238 -r faad58416a3a libinterp/dldfcn/__init_gnuplot__.cc --- a/libinterp/dldfcn/__init_gnuplot__.cc Mon Dec 27 01:17:19 2021 -0600 +++ b/libinterp/dldfcn/__init_gnuplot__.cc Mon Dec 27 01:47:37 2021 -0600 @@ -63,25 +63,6 @@ gnuplot_graphics_toolkit (octave::interpreter& interp) : octave::base_graphics_toolkit ("gnuplot"), m_interpreter (interp) { - static bool warned = false; - - if (! warned) - { - warning_with_id - ("Octave:gnuplot-graphics", - "using the gnuplot graphics toolkit is discouraged\n\ -\n\ -The gnuplot graphics toolkit is not actively maintained and has a number\n\ -of limitations that are ulikely to be fixed. Communication with gnuplot\n\ -uses a one-directional pipe and limited information is passed back to the\n\ -Octave interpreter so most changes made interactively in the plot window\n\ -will not be reflected in the graphics properties managed by Octave. For\n\ -example, if the plot window is closed with a mouse click, Octave will not\n\ -be notified and will not update it's internal list of open figure windows.\n\ -We recommend using the qt toolkit instead.\n"); - - warned = true; - } } ~gnuplot_graphics_toolkit (void) = default; ================================================ FILE: back-octave/oo-changesets/400-7ade2492e023.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640564444 21600 # Sun Dec 26 18:20:44 2021 -0600 # Branch oo-7.0.1 # Node ID 7ade2492e0237c2557ae6e67ee1be88cce8e6982 # Parent 117ebe363f56509f909190fc12aa321e55bea711 # Parent 1e1c91e6cddc3c48870e390b2730c7b586cc8a89 Merge oo-6.0.1 into oo-7.0.1 diff -r 117ebe363f56 -r 7ade2492e023 configure.ac --- a/configure.ac Sat Dec 25 19:16:44 2021 -0800 +++ b/configure.ac Sun Dec 26 18:20:44 2021 -0600 @@ -2909,7 +2909,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $SPARSE_XLDFLAGS $FFTW_XLDFLAGS" diff -r 117ebe363f56 -r 7ade2492e023 libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libgui/src/qt-interpreter-events.cc Sun Dec 26 18:20:44 2021 -0600 @@ -309,6 +309,21 @@ emit edit_variable_signal (QString::fromStdString (expr), val); } + void qt_interpreter_events::show_static_plot (const std::string&, const std::string&) + { + return; + } + + std::string qt_interpreter_events::request_input (const std::string&) + { + return {}; + } + + std::string qt_interpreter_events::request_url (const std::string&, const std::list&, const std::string&, bool&) + { + return {}; + } + bool qt_interpreter_events::confirm_shutdown (void) { QMutexLocker autolock (&m_mutex); @@ -601,6 +616,9 @@ emit clear_history_signal (); } + void qt_interpreter_events::do_clear_screen (void) + { } + void qt_interpreter_events::pre_input_event (void) { } diff -r 117ebe363f56 -r 7ade2492e023 libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h Sat Dec 25 19:16:44 2021 -0800 +++ b/libgui/src/qt-interpreter-events.h Sun Dec 26 18:20:44 2021 -0600 @@ -136,6 +136,12 @@ void edit_variable (const std::string& name, const octave_value& val); + void show_static_plot (const std::string& term, const std::string& content); + + std::string request_input (const std::string&); + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool confirm_shutdown (void); bool prompt_new_edit_file (const std::string& file); @@ -190,6 +196,8 @@ void clear_history (void); + void clear_screen (void); + void pre_input_event (void); void post_input_event (void); diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/event-manager.cc Sun Dec 26 18:20:44 2021 -0600 @@ -46,6 +46,16 @@ OCTAVE_NAMESPACE_BEGIN + bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ ("request_input_enabled"); + return evmgr.request_input_enabled(); + } + + std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ ("request_url"); + return evmgr.request_url(url, param, action, success); + } + static int readline_event_hook (void) { event_manager& evmgr = __get_event_manager__ ("octave_readline_hook"); @@ -878,3 +888,27 @@ } OCTAVE_NAMESPACE_END + +DEFMETHOD (__event_manager_plot_destination__, interp, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (interp.get_event_manager().plot_destination()); +} + +DEFMETHOD (__event_manager_show_static_plot__, interp, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (interp.get_event_manager().show_static_plot(term, content)); +} diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/event-manager.h Sun Dec 26 18:20:44 2021 -0600 @@ -50,6 +50,12 @@ class execution_exception; class symbol_info_list; + enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 + }; + // The methods in this class provide a way to pass signals to the GUI // thread. A GUI that wishes to act on these events should derive // from this class and perform actions in a thread-safe way. In @@ -176,6 +182,13 @@ // confirmation before another action. Could these be reformulated // using the question_dialog action? + bool _request_input_enabled; + virtual std::string request_input (const std::string&) = 0; + virtual std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + + plot_destination_t _plot_destination; + virtual void show_static_plot (const std::string& term, const std::string& content) = 0; + virtual bool confirm_shutdown (void) { return true; } virtual bool prompt_new_edit_file (const std::string& /*file*/) @@ -260,6 +273,8 @@ virtual void clear_history (void) { } + virtual void clear_screen (void) { } + virtual void pre_input_event (void) { } virtual void post_input_event (void) { } @@ -446,6 +461,28 @@ m_instance->update_path_dialog (); } + bool request_input_enabled (void) + { + return enabled () ? instance->_request_input_enabled : false; + } + + plot_destination_t plot_destination (void) + { + return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + } + + bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + instance->show_static_plot (term, content); + return true; + } + else + return false; + } + bool show_preferences (void) { if (enabled ()) @@ -706,6 +743,12 @@ m_instance->clear_history (); } + void clear_screen (void) + { + if (enabled ()) + instance->clear_screen (); + } + void pre_input_event (void) { if (enabled ()) @@ -718,6 +761,21 @@ m_instance->post_input_event (); } + + std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? instance->request_input (prompt) + : std::string (); + } + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? instance->request_url (url, param, action, success) + : std::string (); + } + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) { diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/input.cc Sun Dec 26 18:20:44 2021 -0600 @@ -791,7 +791,12 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + event_manager& evmgr = m_interpreter.get_event_manager (); + if (evmgr.request_input_enabled ()) + retval = evmgr.request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1666,3 +1671,32 @@ } OCTAVE_NAMESPACE_END + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/interpreter.cc Sun Dec 26 18:20:44 2021 -0600 @@ -62,6 +62,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave.h" @@ -619,6 +620,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (*this, options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } // FIXME: we defer creation of the gh_manager object because it diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Sun Dec 26 18:20:44 2021 -0600 @@ -0,0 +1,101 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" +#include "interpreter.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +namespace octave { + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (new octave_json_link(this)) +{ + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + event_manager& evmgr = interp.get_event_manager (); + evmgr.connect_link (_octave_json_link); + evmgr.enable (); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link->receive_message(name, jobj); +} + +} // namespace octave diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Sun Dec 26 18:20:44 2021 -0600 @@ -0,0 +1,37 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +namespace octave { + +class interpreter; + +class json_main { +public: + json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + + std::shared_ptr _octave_json_link; +}; + +} // namespace octave + +#endif diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Sun Dec 26 18:20:44 2021 -0600 @@ -0,0 +1,264 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +namespace octave { + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_symbol_info_list(const symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_filter_list(const interpreter_events::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_symbol_info(const symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + + JSON_MAP_T m; + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (size_t i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} + +} // namespace octave diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Sun Dec 26 18:20:44 2021 -0600 @@ -0,0 +1,64 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "syminfo.h" +#include "event-manager.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +namespace octave { + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const interpreter_events::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_symbol_info(const symbol_info element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +} // namespace octave + +#endif diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/module.mk Sun Dec 26 18:20:44 2021 -0600 @@ -45,6 +45,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/latex-text-renderer.h \ %reldir%/load-path.h \ @@ -76,6 +78,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ %reldir%/pr-output.h \ @@ -186,6 +189,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ %reldir%/inv.cc \ @@ -225,6 +230,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/ordqz.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 18:20:44 2021 -0600 @@ -0,0 +1,411 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +namespace octave { + +octave_json_link::octave_json_link(json_main* __json_main) + : interpreter_events (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); + +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); + +// return true; +// } + +bool octave_json_link::copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); + +// return message_dialog_queue.dequeue(); +// } + +std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::directory_changed(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + +void octave_json_link::execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +uint8NDArray octave_json_link::get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::set_workspace(bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::pre_input_event(void) { + // noop +} + +void octave_json_link::post_input_event(void) { + // noop +} + +void octave_json_link::enter_debugger_event(const std::string& fcn_name, const std::string& fcn_file_name, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, fcn_name, string); + JSON_MAP_SET(m, fcn_file_name, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } + +void octave_json_link::show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +std::string octave_json_link::gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + +void octave_json_link::show_doc(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); +} + +void octave_json_link::register_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::unregister_doc (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} + +void octave_json_link::show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + +} // namespace octave diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 18:20:44 2021 -0600 @@ -0,0 +1,216 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "event-manager.h" +#include "json-util.h" +#include "oct-mutex.h" + +class string_vector; + +namespace octave { + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + mutex _mutex; +}; + +class octave_json_link : public interpreter_events +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + std::string request_input (const std::string& prompt) override; + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + bool confirm_shutdown (void) override; + + bool copy_image_to_clipboard (const std::string& file) override; + + bool edit_file (const std::string& file) override; + bool prompt_new_edit_file (const std::string& file) override; + + std::string + question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + int + debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + void directory_changed (const std::string& dir) override; + + void file_remove (const std::string& old_name, const std::string& new_name) override; + void file_renamed (bool) override; + + void execute_command_in_terminal (const std::string& command) override; + + uint8NDArray get_named_icon (const std::string& icon_name) override; + + void set_workspace (bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) override; + + void clear_workspace (void) override; + + void set_history (const string_vector& hist) override; + void append_history (const std::string& hist_entry) override; + void clear_history (void) override; + + void clear_screen (void) override; + + void pre_input_event (void) override; + void post_input_event (void) override; + + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + void execute_in_debugger_event (const std::string& file, int line) override; + void exit_debugger_event (void) override; + + void update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + void show_preferences (void) override; + + std::string gui_preference (const std::string& key, const std::string& value) override; + + void show_doc (const std::string& file) override; + + void register_doc (const std::string& file) override; + + void unregister_doc (const std::string& file) override; + + void edit_variable (const std::string& name, const octave_value& val) override; + + void show_static_plot (const std::string& term, + const std::string& content) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +} // namespace octave + +#endif diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/syscalls.cc Sun Dec 26 18:20:44 2021 -0600 @@ -151,9 +151,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -476,42 +475,6 @@ return retval; } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (interp.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/sysdep.cc Sun Dec 26 18:20:44 2021 -0600 @@ -75,6 +75,7 @@ #include "defun.h" #include "error.h" #include "errwarn.h" +#include "event-manager.h" #include "input.h" #include "interpreter-private.h" #include "octave.h" @@ -719,7 +720,7 @@ // Read one character from the terminal. - int kbhit (bool wait) + int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -746,13 +747,24 @@ set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + event_manager& evmgr = __get_event_manager__ ("kbhit"); + if (evmgr.request_input_enabled ()) { + std::string line = evmgr.request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). set_interrupt_handler (saved_interrupt_handler, true); @@ -810,6 +822,9 @@ { bool skip_redisplay = true; + octave::event_manager& evmgr = octave::__get_event_manager__ ("clc"); + evmgr.clear_screen(); + command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1238,7 +1253,7 @@ Fdrawnow (interp); - int c = kbhit (args.length () == 0); + int c = kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/sysdep.h Sun Dec 26 18:20:44 2021 -0600 @@ -49,7 +49,7 @@ extern OCTINTERP_API int pclose (FILE *f); - extern OCTINTERP_API int kbhit (bool wait = true); + extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); diff -r 117ebe363f56 -r 7ade2492e023 libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/corefcn/utils.cc Sun Dec 26 18:20:44 2021 -0600 @@ -1584,7 +1584,7 @@ if (do_graphics_events) gh_mgr.process_events (); - c = kbhit (false); + c = kbhit ("press enter to continue", false); } } else diff -r 117ebe363f56 -r 7ade2492e023 libinterp/octave.cc --- a/libinterp/octave.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/octave.cc Sun Dec 26 18:20:44 2021 -0600 @@ -184,6 +184,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -418,6 +428,14 @@ sysdep_init (); } + bool application::link_enabled (void) const + { + if (m_interpreter) { + event_manager& evmgr = m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; + } + int cli_application::execute (void) { interpreter& interp = create_interpreter (); @@ -440,7 +458,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (application::is_gui_running ()); + return ovl (application::is_link_enabled ()); } /* diff -r 117ebe363f56 -r 7ade2492e023 libinterp/octave.h --- a/libinterp/octave.h Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/octave.h Sun Dec 26 18:20:44 2021 -0600 @@ -82,6 +82,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -112,6 +114,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -220,6 +224,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; @@ -233,6 +245,7 @@ // both) of them... class interpreter; + class event_manager; // Base class for an Octave application. @@ -282,6 +295,8 @@ virtual bool gui_running (void) const { return false; } virtual void gui_running (bool) { } + bool link_enabled (void) const; + void program_invocation_name (const std::string& nm) { m_program_invocation_name = nm; } void program_name (const std::string& nm) { m_program_name = nm; } @@ -314,6 +329,11 @@ return s_instance ? s_instance->gui_running () : false; } + static bool is_link_enabled (void) + { + return instance ? instance->link_enabled () : false; + } + // Convenience functions. static bool forced_interactive (void); diff -r 117ebe363f56 -r 7ade2492e023 libinterp/options.h --- a/libinterp/options.h Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/options.h Sun Dec 26 18:20:44 2021 -0600 @@ -46,17 +46,19 @@ #define IMAGE_PATH_OPTION 7 #define INFO_FILE_OPTION 8 #define INFO_PROG_OPTION 9 -#define LINE_EDITING_OPTION 10 -#define NO_GUI_OPTION 11 -#define NO_GUI_LIBS_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define SERVER_OPTION 18 -#define TEXI_MACROS_FILE_OPTION 19 -#define TRADITIONAL_OPTION 20 +#define JSON_SOCK_OPTION 10 +#define JSON_MAX_LEN_OPTION 11 +#define LINE_EDITING_OPTION 12 +#define NO_GUI_OPTION 13 +#define NO_GUI_LIBS_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define SERVER_OPTION 20 +#define TEXI_MACROS_FILE_OPTION 21 +#define TRADITIONAL_OPTION 22 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, nullptr, TRADITIONAL_OPTION }, @@ -74,6 +76,8 @@ { "info-file", octave_required_arg, nullptr, INFO_FILE_OPTION }, { "info-program", octave_required_arg, nullptr, INFO_PROG_OPTION }, { "interactive", octave_no_arg, nullptr, 'i' }, + { "json-sock", octave_required_arg, nullptr, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, nullptr, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, nullptr, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, nullptr, NO_GUI_OPTION }, { "no-gui-libs", octave_no_arg, nullptr, NO_GUI_LIBS_OPTION }, diff -r 117ebe363f56 -r 7ade2492e023 libinterp/usage.h --- a/libinterp/usage.h Sat Dec 25 19:16:44 2021 -0800 +++ b/libinterp/usage.h Sun Dec 26 18:20:44 2021 -0600 @@ -37,11 +37,11 @@ "octave [-HVWdfhiqvx] [--debug] [--doc-cache-file file] [--echo-commands]\n\ [--eval CODE] [--exec-path path] [--experimental-terminal-widget]\n\ [--gui] [--help] [--image-path path] [--info-file file]\n\ - [--info-program prog] [--interactive] [--line-editing] [--no-gui]\n\ - [--no-history] [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--server] [--silent] [--traditional]\n\ - [--verbose] [--version] [file]"; + [--info-program prog] [--interactive] [--json-sock] [--json-max-len] \n\ + [--line-editing] [--no-gui] [--no-history] [--no-init-file] \n\ + [--no-init-path] [--no-line-editing] [--no-site-file] \n\ + [--no-window-system] [--norc] [-p path] [--path path] [--persist] \n\ + [--server] [--silent] [--traditional] [--verbose] [--version] [file]"; // Usage message with extra help. @@ -69,6 +69,8 @@ --info-file FILE Use top-level info file FILE.\n\ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 117ebe363f56 -r 7ade2492e023 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/liboctave/util/oct-mutex.cc Sun Dec 26 18:20:44 2021 -0600 @@ -58,6 +58,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -68,11 +80,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -90,8 +104,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -123,11 +148,19 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&m_pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&m_pm); + pthread_mutex_destroy (&m_pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -145,8 +178,19 @@ return (pthread_mutex_trylock (&m_pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &m_pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t m_pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 117ebe363f56 -r 7ade2492e023 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Sat Dec 25 19:16:44 2021 -0800 +++ b/liboctave/util/oct-mutex.h Sun Dec 26 18:20:44 2021 -0600 @@ -49,6 +49,10 @@ virtual void unlock (void); virtual bool try_lock (void); + + virtual void cond_wait (void); + + virtual void cond_signal (void); }; class @@ -79,6 +83,16 @@ return m_rep->try_lock (); } + void cond_wait (void) + { + m_rep->cond_wait (); + } + + void cond_signal (void) + { + m_rep->cond_signal (); + } + protected: std::shared_ptr m_rep; }; diff -r 117ebe363f56 -r 7ade2492e023 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sat Dec 25 19:16:44 2021 -0800 +++ b/liboctave/util/url-transfer.cc Sun Dec 26 18:20:44 2021 -0600 @@ -36,6 +36,7 @@ #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" +#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" #include "version.h" @@ -48,6 +49,10 @@ namespace octave { + // Forward declaration for event_manager + extern bool __event_manager_request_input_enabled__(); + extern std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success); + base_url_transfer::base_url_transfer (void) : m_host_or_url (), m_valid (false), m_ftp (false), m_ascii_mode (false), m_ok (true), m_errmsg (), @@ -228,6 +233,86 @@ return file_list; } + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + m_valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + m_valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + m_valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform_action (param, "get"); + } + + void http_post (const Array& param) { + perform_action (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform_action (param, action); + } + +private: + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (__event_manager_request_input_enabled__()) { + bool success; + std::string result = __event_manager_request_url__(url, paramList, action, success); + if (success) { + process_success(result); + } else { + m_ok = false; + m_errmsg = result; + } + } else { + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + size_t outlen; + bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + if (!b64_ok) { + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; + } else { + m_curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -918,17 +1003,30 @@ # define REP_CLASS base_url_transfer #endif - url_transfer::url_transfer (void) : m_rep (new REP_CLASS ()) - { } + url_transfer::url_transfer (void) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new REP_CLASS()); + } + } url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : m_rep (new REP_CLASS (host, user, passwd, os)) - { } + const std::string& passwd, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new REP_CLASS(host, user, passwd, os)); + } + } - url_transfer::url_transfer (const std::string& url, std::ostream& os) - : m_rep (new REP_CLASS (url, os)) - { } + url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new REP_CLASS(url, os)); + } + } #undef REP_CLASS diff -r 117ebe363f56 -r 7ade2492e023 scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Sat Dec 25 19:16:44 2021 -0800 +++ b/scripts/help/__unimplemented__.m Sun Dec 26 18:20:44 2021 -0600 @@ -45,7 +45,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -524,6 +547,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -566,13 +590,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run 'pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 117ebe363f56 -r 7ade2492e023 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Sat Dec 25 19:16:44 2021 -0800 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Sun Dec 26 18:20:44 2021 -0600 @@ -32,9 +32,84 @@ if (nargin < 1 || nargin == 2) print_usage (); - endif + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __event_manager_plot_destination__ () == 0 || + __event_manager_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif - if (nargin >= 3 && nargin <= 4) + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __event_manager_plot_destination__ () == 1 || + __event_manager_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __event_manager_show_static_plot__ (term, a); + endif + endif + endif + + endif +endfunction + +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -70,44 +145,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/401-1b33dc797ec9.hg.txt ================================================ # HG changeset patch # User Octave Online Team # Date 1640567764 21600 # Sun Dec 26 19:16:04 2021 -0600 # Branch oo-7.0.1 # Node ID 1b33dc797ec9b06aa971361d838f656fb7cc41ac # Parent 7ade2492e0237c2557ae6e67ee1be88cce8e6982 Fix compile errors diff -r 7ade2492e023 -r 1b33dc797ec9 libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Sun Dec 26 18:20:44 2021 -0600 +++ b/libinterp/corefcn/event-manager.h Sun Dec 26 19:16:04 2021 -0600 @@ -183,11 +183,16 @@ // using the question_dialog action? bool _request_input_enabled; - virtual std::string request_input (const std::string&) = 0; - virtual std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) = 0; + virtual std::string request_input (const std::string&) + { + return ""; + } + virtual std::string request_url (const std::string& /*url*/, const std::list& /*param*/, const std::string& /*action*/, bool& /*success*/) { + return ""; + } plot_destination_t _plot_destination; - virtual void show_static_plot (const std::string& term, const std::string& content) = 0; + virtual void show_static_plot (const std::string& /*term*/, const std::string& /*content*/) { } virtual bool confirm_shutdown (void) { return true; } @@ -463,12 +468,12 @@ bool request_input_enabled (void) { - return enabled () ? instance->_request_input_enabled : false; + return enabled () ? m_instance->_request_input_enabled : false; } plot_destination_t plot_destination (void) { - return enabled () ? instance->_plot_destination : TERMINAL_ONLY; + return enabled () ? m_instance->_plot_destination : TERMINAL_ONLY; } bool @@ -476,7 +481,7 @@ { if (enabled ()) { - instance->show_static_plot (term, content); + m_instance->show_static_plot (term, content); return true; } else @@ -746,7 +751,7 @@ void clear_screen (void) { if (enabled ()) - instance->clear_screen (); + m_instance->clear_screen (); } void pre_input_event (void) @@ -765,14 +770,14 @@ std::string request_input (const std::string& prompt) { return request_input_enabled () - ? instance->request_input (prompt) + ? m_instance->request_input (prompt) : std::string (); } std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) { return request_input_enabled () - ? instance->request_url (url, param, action, success) + ? m_instance->request_url (url, param, action, success) : std::string (); } diff -r 7ade2492e023 -r 1b33dc797ec9 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Sun Dec 26 18:20:44 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 19:16:04 2021 -0600 @@ -325,17 +325,18 @@ return retval; } -void octave_json_link::show_doc(const std::string& file) { +bool octave_json_link::show_documentation(const std::string& file) { // Triggered on "doc" command _publish_message("show-doc", json_util::from_string(file)); + return true; } -void octave_json_link::register_doc (const std::string& file) { +void octave_json_link::register_documentation (const std::string& file) { // Triggered by the GUI documentation viewer? _publish_message("register-doc", json_util::from_string(file)); } -void octave_json_link::unregister_doc (const std::string& file) { +void octave_json_link::unregister_documentation (const std::string& file) { // Triggered by the GUI documentation viewer? _publish_message("unregister-doc", json_util::from_string(file)); } diff -r 7ade2492e023 -r 1b33dc797ec9 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Sun Dec 26 18:20:44 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 19:16:04 2021 -0600 @@ -139,11 +139,11 @@ std::string gui_preference (const std::string& key, const std::string& value) override; - void show_doc (const std::string& file) override; + bool show_documentation (const std::string& file) override; - void register_doc (const std::string& file) override; + void register_documentation (const std::string& file) override; - void unregister_doc (const std::string& file) override; + void unregister_documentation (const std::string& file) override; void edit_variable (const std::string& name, const octave_value& val) override; diff -r 7ade2492e023 -r 1b33dc797ec9 libinterp/octave.h --- a/libinterp/octave.h Sun Dec 26 18:20:44 2021 -0600 +++ b/libinterp/octave.h Sun Dec 26 19:16:04 2021 -0600 @@ -331,7 +331,7 @@ static bool is_link_enabled (void) { - return instance ? instance->link_enabled () : false; + return s_instance ? s_instance->link_enabled () : false; } // Convenience functions. diff -r 7ade2492e023 -r 1b33dc797ec9 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sun Dec 26 18:20:44 2021 -0600 +++ b/liboctave/util/url-transfer.cc Sun Dec 26 19:16:04 2021 -0600 @@ -31,12 +31,12 @@ #include #include +#include "base64-wrappers.h" #include "dir-ops.h" #include "file-ops.h" #include "file-stat.h" #include "lo-sysdep.h" #include "oct-env.h" -#include "gnulib/lib/base64.h" #include "unwind-prot.h" #include "url-transfer.h" #include "version.h" @@ -300,8 +300,8 @@ // Use the base64 implementation from gnulib, which is already an Octave dependency. const char *inc = &(result[0]); char *out; - size_t outlen; - bool b64_ok = base64_decode_alloc(inc, result.length(), &out, &outlen); + std::ptrdiff_t outlen; + bool b64_ok = octave_base64_decode_alloc_wrapper(inc, result.length(), &out, &outlen); if (!b64_ok) { m_ok = false; m_errmsg = "failed decoding base64 from octave_link"; ================================================ FILE: back-octave/oo-changesets/402-b01fa2864d4d.hg.txt ================================================ # HG changeset patch # User Octave Online Team # Date 1640569694 21600 # Sun Dec 26 19:48:14 2021 -0600 # Branch oo-7.0.1 # Node ID b01fa2864d4d45ba10bcb1e59fe84bb54330ab17 # Parent 1b33dc797ec9b06aa971361d838f656fb7cc41ac Fix linker errors diff -r 1b33dc797ec9 -r b01fa2864d4d libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Sun Dec 26 19:16:04 2021 -0600 +++ b/libinterp/corefcn/event-manager.cc Sun Dec 26 19:48:14 2021 -0600 @@ -887,8 +887,6 @@ return ovl (); } -OCTAVE_NAMESPACE_END - DEFMETHOD (__event_manager_plot_destination__, interp, , , doc: /* -*- texinfo -*- @deftypefn {} {} __event_manager_plot_destination__ () @@ -912,3 +910,5 @@ std::string content = args(1).string_value(); return ovl (interp.get_event_manager().show_static_plot(term, content)); } + +OCTAVE_NAMESPACE_END diff -r 1b33dc797ec9 -r b01fa2864d4d libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Sun Dec 26 19:16:04 2021 -0600 +++ b/libinterp/corefcn/input.cc Sun Dec 26 19:48:14 2021 -0600 @@ -1670,8 +1670,6 @@ return input_sys.auto_repeat_debug_command (args, nargout); } -OCTAVE_NAMESPACE_END - DEFUN (current_command_number, args, , doc: /* -*- texinfo -*- @deftypefn {} {@var{val} =} current_command_number () @@ -1700,3 +1698,5 @@ return ovl(n); } } + +OCTAVE_NAMESPACE_END ================================================ FILE: back-octave/oo-changesets/403-2813cb96e10f.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640570532 21600 # Sun Dec 26 20:02:12 2021 -0600 # Branch oo-7.0.1 # Node ID 2813cb96e10f0a2b1ec6903a6dbf7c368ac323a1 # Parent b01fa2864d4d45ba10bcb1e59fe84bb54330ab17 Add have_dialogs diff -r b01fa2864d4d -r 2813cb96e10f libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Sun Dec 26 19:48:14 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 20:02:12 2021 -0600 @@ -125,6 +125,13 @@ // return message_dialog_queue.dequeue(); // } +bool octave_json_link::have_dialogs() const { + // Triggered in "inputdlg" and similar functions to check for dialog support + _publish_message("have-dialog", json_util::empty()); + + return have_dialogs_queue.dequeue(); +} + std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { // Triggered in "questdlg" JSON_MAP_T m; @@ -372,6 +379,10 @@ bool answer = json_util::to_boolean(jobj); confirm_shutdown_queue.enqueue(answer); } + else if (name == "have-dialogs-answer"){ + bool answer = json_util::to_boolean(jobj); + have_dialogs_queue.enqueue(answer); + } else if (name == "prompt-new-edit-file-answer"){ bool answer = json_util::to_boolean(jobj); prompt_new_edit_file_queue.enqueue(answer); diff -r b01fa2864d4d -r 2813cb96e10f libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Sun Dec 26 19:48:14 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 20:02:12 2021 -0600 @@ -71,6 +71,8 @@ bool edit_file (const std::string& file) override; bool prompt_new_edit_file (const std::string& file) override; + bool have_dialogs (void) override; + std::string question_dialog (const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, @@ -161,6 +163,7 @@ json_queue request_input_queue; json_queue > request_url_queue; json_queue confirm_shutdown_queue; + json_queue have_dialogs_queue; json_queue prompt_new_edit_file_queue; json_queue message_dialog_queue; json_queue question_dialog_queue; ================================================ FILE: back-octave/oo-changesets/404-acb523f25bb9.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640575083 21600 # Sun Dec 26 21:18:03 2021 -0600 # Branch oo-7.0.1 # Node ID acb523f25bb964dbc67a871ea27b7b7366203f26 # Parent 2813cb96e10f0a2b1ec6903a6dbf7c368ac323a1 Update methods in octave-json-link.h diff -r 2813cb96e10f -r acb523f25bb9 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Sun Dec 26 20:02:12 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 21:18:03 2021 -0600 @@ -189,6 +189,11 @@ return file_dialog_queue.dequeue(); } +void octave_json_link::update_path_dialog(void) { + // Triggered in "rehash" + _publish_message("update-path-dialog", json_util::empty()); +} + int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { // This endpoint might be unused? (No references) JSON_MAP_T m; diff -r 2813cb96e10f -r acb523f25bb9 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Sun Dec 26 20:02:12 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 21:18:03 2021 -0600 @@ -61,15 +61,9 @@ ~octave_json_link (void); - std::string request_input (const std::string& prompt) override; - std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; - - bool confirm_shutdown (void) override; - - bool copy_image_to_clipboard (const std::string& file) override; - - bool edit_file (const std::string& file) override; - bool prompt_new_edit_file (const std::string& file) override; + // TODO + // void start_gui (bool gui_app = false); + // void close_gui (void); bool have_dialogs (void) override; @@ -100,57 +94,115 @@ const std::string &filename, const std::string &pathname, const std::string& multimode) override; + void update_path_dialog (void) override; + + void show_preferences (void) override; + + // TODO: + // void apply_preferences (void) override; + + // TODO: + // void show_terminal_window (void) override; + + bool show_documentation (const std::string& file) override; + + // TODO: + // void show_file_browser (void); + + // TODO: + // void show_command_history (void); + + void show_workspace (void); + + // TODO: + // void show_community_news (int serial); + // void show_release_notes (void); + + bool edit_file (const std::string& file) override; + + void edit_variable (const std::string& name, const octave_value& val) override; + + std::string request_input (const std::string& prompt) override; + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + void show_static_plot (const std::string& term, const std::string& content) override; + + bool confirm_shutdown (void) override; + + bool prompt_new_edit_file (const std::string& file) override; + int debug_cd_or_addpath_error (const std::string& file, const std::string& dir, bool addpath_option) override; + uint8NDArray get_named_icon (const std::string& icon_name) override; + + std::string gui_preference (const std::string& key, const std::string& value) override; + + bool copy_image_to_clipboard (const std::string& file) override; + + // TODO: + // void focus_window (const std::string win_name); + + void execute_command_in_terminal (const std::string& command) override; + + void register_documentation (const std::string& file) override; + + void unregister_documentation (const std::string& file) override; + + // TODO: + // void interpreter_output (const std::string& msg); + + // TODO: + // void display_exception (const execution_exception& ee, bool beep); + + // TODO: + // void gui_status_update (const std::string& feature, const std::string& status); + + // TODO: + // void update_gui_lexer (void); + void directory_changed (const std::string& dir) override; void file_remove (const std::string& old_name, const std::string& new_name) override; + void file_renamed (bool) override; - void execute_command_in_terminal (const std::string& command) override; - - uint8NDArray get_named_icon (const std::string& icon_name) override; - void set_workspace (bool top_level, bool debug, const symbol_info_list& ws, bool update_variable_editor) override; void clear_workspace (void) override; + // TODO: + // void update_prompt (const std::string& prompt); + void set_history (const string_vector& hist) override; + void append_history (const std::string& hist_entry) override; + void clear_history (void) override; void clear_screen (void) override; void pre_input_event (void) override; + void post_input_event (void) override; void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + void execute_in_debugger_event (const std::string& file, int line) override; + void exit_debugger_event (void) override; void update_breakpoint (bool insert, const std::string& file, int line, const std::string& cond) override; - void show_preferences (void) override; - - std::string gui_preference (const std::string& key, const std::string& value) override; - - bool show_documentation (const std::string& file) override; - - void register_documentation (const std::string& file) override; - - void unregister_documentation (const std::string& file) override; - - void edit_variable (const std::string& name, const octave_value& val) override; - - void show_static_plot (const std::string& term, - const std::string& content) override; + // TODO: + // void interpreter_interrupted (void) override; // Custom methods void receive_message (const std::string& name, JSON_OBJECT_T jobj); ================================================ FILE: back-octave/oo-changesets/405-6ad34b0b69e1.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640576162 21600 # Sun Dec 26 21:36:02 2021 -0600 # Branch oo-7.0.1 # Node ID 6ad34b0b69e19c2ce3086daf148fce7bc7d51b2e # Parent acb523f25bb964dbc67a871ea27b7b7366203f26 Support more methods in octave-json-link diff -r acb523f25bb9 -r 6ad34b0b69e1 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Sun Dec 26 21:18:03 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 21:36:02 2021 -0600 @@ -205,6 +205,34 @@ return debug_cd_or_addpath_error_queue.dequeue(); } +void octave_json_link::focus_window(const std::string& win_name) { + // Triggered in "commandhistory", "commandwindow", "filebrowser", "workspace" + JSON_MAP_T m; + JSON_MAP_SET(m, win_name, string); + _publish_message("focus-window", json_util::from_map(m)); +} + +void octave_json_link::display_exception(const execution_exception& ee, bool beep) { + // Triggered in various places in libinterp + JSON_MAP_T m; + JSON_MAP_SET(m, ee, string); + JSON_MAP_SET(m, beep, boolean); + _publish_message("display-exception", json_util::from_map(m)); +} + +void octave_json_link::gui_status_update(const std::string& feature, const std::string& status) { + // Triggered in __profiler_enable__ + JSON_MAP_T m; + JSON_MAP_SET(m, feature, string); + JSON_MAP_SET(m, status, string); + _publish_message("gui-status-update", json_util::from_map(m)); +} + +void octave_json_link::update_gui_lexer(void) { + // Triggered in "load_packages_and_dependencies" + _publish_message("update-gui-lexer", json_util::empty()); +} + void octave_json_link::directory_changed(const std::string& dir) { // This endpoint might be unused? (References appear only in libgui) JSON_MAP_T m; diff -r acb523f25bb9 -r 6ad34b0b69e1 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Sun Dec 26 21:18:03 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 21:36:02 2021 -0600 @@ -112,7 +112,7 @@ // TODO: // void show_command_history (void); - void show_workspace (void); + void show_workspace (void) override; // TODO: // void show_community_news (int serial); @@ -143,8 +143,7 @@ bool copy_image_to_clipboard (const std::string& file) override; - // TODO: - // void focus_window (const std::string win_name); + void focus_window (const std::string win_name); void execute_command_in_terminal (const std::string& command) override; @@ -155,14 +154,11 @@ // TODO: // void interpreter_output (const std::string& msg); - // TODO: - // void display_exception (const execution_exception& ee, bool beep); + void display_exception (const execution_exception& ee, bool beep) override; - // TODO: - // void gui_status_update (const std::string& feature, const std::string& status); + void gui_status_update (const std::string& feature, const std::string& status); - // TODO: - // void update_gui_lexer (void); + void update_gui_lexer (void); void directory_changed (const std::string& dir) override; ================================================ FILE: back-octave/oo-changesets/406-d0df6f16f41e.hg.txt ================================================ # HG changeset patch # User Octave Online Team # Date 1640581133 21600 # Sun Dec 26 22:58:53 2021 -0600 # Branch oo-7.0.1 # Node ID d0df6f16f41ed3d39e4e40bf8080d143e4726098 # Parent 6ad34b0b69e19c2ce3086daf148fce7bc7d51b2e Fixing build issues diff -r 6ad34b0b69e1 -r d0df6f16f41e libinterp/corefcn/json-main.cc --- a/libinterp/corefcn/json-main.cc Sun Dec 26 21:36:02 2021 -0600 +++ b/libinterp/corefcn/json-main.cc Sun Dec 26 22:58:53 2021 -0600 @@ -37,6 +37,7 @@ // Note: this passes ownership to octave_link event_manager& evmgr = interp.get_event_manager (); evmgr.connect_link (_octave_json_link); + evmgr.install_qt_event_handlers (_octave_json_link); evmgr.enable (); // Open UNIX socket file descriptor diff -r 6ad34b0b69e1 -r d0df6f16f41e libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Sun Dec 26 21:36:02 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 22:58:53 2021 -0600 @@ -127,9 +127,7 @@ bool octave_json_link::have_dialogs() const { // Triggered in "inputdlg" and similar functions to check for dialog support - _publish_message("have-dialog", json_util::empty()); - - return have_dialogs_queue.dequeue(); + return true; } std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { @@ -205,7 +203,7 @@ return debug_cd_or_addpath_error_queue.dequeue(); } -void octave_json_link::focus_window(const std::string& win_name) { +void octave_json_link::focus_window(const std::string win_name) { // Triggered in "commandhistory", "commandwindow", "filebrowser", "workspace" JSON_MAP_T m; JSON_MAP_SET(m, win_name, string); @@ -214,8 +212,11 @@ void octave_json_link::display_exception(const execution_exception& ee, bool beep) { // Triggered in various places in libinterp + std::ostringstream buf; + ee.display (buf); + std::string ee_str = buf.str(); JSON_MAP_T m; - JSON_MAP_SET(m, ee, string); + JSON_MAP_SET(m, ee_str, string); JSON_MAP_SET(m, beep, boolean); _publish_message("display-exception", json_util::from_map(m)); } diff -r 6ad34b0b69e1 -r d0df6f16f41e libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Sun Dec 26 21:36:02 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 22:58:53 2021 -0600 @@ -62,10 +62,10 @@ ~octave_json_link (void); // TODO - // void start_gui (bool gui_app = false); - // void close_gui (void); + // void start_gui (bool gui_app = false) override; + // void close_gui (void) override; - bool have_dialogs (void) override; + bool have_dialogs (void) const override; std::string question_dialog (const std::string& msg, const std::string& title, @@ -107,16 +107,17 @@ bool show_documentation (const std::string& file) override; // TODO: - // void show_file_browser (void); + // void show_file_browser (void) override; + + // TODO: + // void show_command_history (void) override; // TODO: - // void show_command_history (void); - - void show_workspace (void) override; + // void show_workspace (void) override; // TODO: - // void show_community_news (int serial); - // void show_release_notes (void); + // void show_community_news (int serial) override; + // void show_release_notes (void) override; bool edit_file (const std::string& file) override; @@ -143,7 +144,7 @@ bool copy_image_to_clipboard (const std::string& file) override; - void focus_window (const std::string win_name); + void focus_window (const std::string win_name) override; void execute_command_in_terminal (const std::string& command) override; @@ -152,13 +153,13 @@ void unregister_documentation (const std::string& file) override; // TODO: - // void interpreter_output (const std::string& msg); + // void interpreter_output (const std::string& msg) override; void display_exception (const execution_exception& ee, bool beep) override; - void gui_status_update (const std::string& feature, const std::string& status); + void gui_status_update (const std::string& feature, const std::string& status) override; - void update_gui_lexer (void); + void update_gui_lexer (void) override; void directory_changed (const std::string& dir) override; @@ -173,7 +174,7 @@ void clear_workspace (void) override; // TODO: - // void update_prompt (const std::string& prompt); + // void update_prompt (const std::string& prompt) override; void set_history (const string_vector& hist) override; ================================================ FILE: back-octave/oo-changesets/407-df206dd11399.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640581766 21600 # Sun Dec 26 23:09:26 2021 -0600 # Branch oo-7.0.1 # Node ID df206dd113996f60a2559ad898b96b1467a596b5 # Parent d0df6f16f41ed3d39e4e40bf8080d143e4726098 Remove unused queue diff -r d0df6f16f41e -r df206dd11399 libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Sun Dec 26 22:58:53 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Dec 26 23:09:26 2021 -0600 @@ -413,10 +413,6 @@ bool answer = json_util::to_boolean(jobj); confirm_shutdown_queue.enqueue(answer); } - else if (name == "have-dialogs-answer"){ - bool answer = json_util::to_boolean(jobj); - have_dialogs_queue.enqueue(answer); - } else if (name == "prompt-new-edit-file-answer"){ bool answer = json_util::to_boolean(jobj); prompt_new_edit_file_queue.enqueue(answer); diff -r d0df6f16f41e -r df206dd11399 libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Sun Dec 26 22:58:53 2021 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Dec 26 23:09:26 2021 -0600 @@ -212,7 +212,6 @@ json_queue request_input_queue; json_queue > request_url_queue; json_queue confirm_shutdown_queue; - json_queue have_dialogs_queue; json_queue prompt_new_edit_file_queue; json_queue message_dialog_queue; json_queue question_dialog_queue; ================================================ FILE: back-octave/oo-changesets/408-8184a51579f3.hg.txt ================================================ # HG changeset patch # User Shane F. Carr # Date 1640581778 21600 # Sun Dec 26 23:09:38 2021 -0600 # Branch oo-7.0.1 # Node ID 8184a51579f351cfe3809445b795d601143cd3c6 # Parent df206dd113996f60a2559ad898b96b1467a596b5 Remove gnuplot warning diff -r df206dd11399 -r 8184a51579f3 libinterp/dldfcn/__init_gnuplot__.cc --- a/libinterp/dldfcn/__init_gnuplot__.cc Sun Dec 26 23:09:26 2021 -0600 +++ b/libinterp/dldfcn/__init_gnuplot__.cc Sun Dec 26 23:09:38 2021 -0600 @@ -65,25 +65,6 @@ gnuplot_graphics_toolkit (octave::interpreter& interp) : octave::base_graphics_toolkit ("gnuplot"), m_interpreter (interp) { - static bool warned = false; - - if (! warned) - { - warning_with_id - ("Octave:gnuplot-graphics", - "using the gnuplot graphics toolkit is discouraged\n\ -\n\ -The gnuplot graphics toolkit is not actively maintained and has a number\n\ -of limitations that are unlikely to be fixed. Communication with gnuplot\n\ -uses a one-directional pipe and limited information is passed back to the\n\ -Octave interpreter so most changes made interactively in the plot window\n\ -will not be reflected in the graphics properties managed by Octave. For\n\ -example, if the plot window is closed with a mouse click, Octave will not\n\ -be notified and will not update its internal list of open figure windows.\n\ -The qt toolkit is recommended instead.\n"); - - warned = true; - } } ~gnuplot_graphics_toolkit (void) = default; ================================================ FILE: back-octave/oo-changesets/420-4c3d80dd9e65.hg.txt ================================================ # HG changeset patch # User username = Octave Online Team # Date 1672382380 21600 # Fri Dec 30 00:39:40 2022 -0600 # Branch stable # Node ID 4c3d80dd9e65ced0f06ce25e13cf7ef9d481f954 # Parent 601e7a142a15c7995dde159d96df317367c8d85f # Parent 48927d553c9d60228cc8e181add56e84c37cbd6d Merge oo-7.0.1 into oo-7.4 diff -r 601e7a142a15 -r 4c3d80dd9e65 configure.ac --- a/configure.ac Thu Dec 29 16:10:07 2022 +0100 +++ b/configure.ac Fri Dec 30 00:39:40 2022 -0600 @@ -2979,7 +2979,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $SPARSE_XLDFLAGS $FFTW_XLDFLAGS" diff -r 601e7a142a15 -r 4c3d80dd9e65 libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libgui/src/qt-interpreter-events.cc Fri Dec 30 00:39:40 2022 -0600 @@ -309,6 +309,21 @@ emit edit_variable_signal (QString::fromStdString (expr), val); } +void qt_interpreter_events::show_static_plot (const std::string&, const std::string&) +{ + return; +} + +std::string qt_interpreter_events::request_input (const std::string&) +{ + return {}; +} + +std::string qt_interpreter_events::request_url (const std::string&, const std::list&, const std::string&, bool&) +{ + return {}; +} + bool qt_interpreter_events::confirm_shutdown (void) { QMutexLocker autolock (&m_mutex); @@ -604,6 +619,9 @@ emit clear_history_signal (); } +void qt_interpreter_events::do_clear_screen (void) +{ } + void qt_interpreter_events::pre_input_event (void) { } diff -r 601e7a142a15 -r 4c3d80dd9e65 libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h Thu Dec 29 16:10:07 2022 +0100 +++ b/libgui/src/qt-interpreter-events.h Fri Dec 30 00:39:40 2022 -0600 @@ -136,6 +136,12 @@ void edit_variable (const std::string& name, const octave_value& val); + void show_static_plot (const std::string& term, const std::string& content); + + std::string request_input (const std::string&); + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool confirm_shutdown (void); bool prompt_new_edit_file (const std::string& file); @@ -190,6 +196,8 @@ void clear_history (void); + void clear_screen (void); + void pre_input_event (void); void post_input_event (void); diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/event-manager.cc Fri Dec 30 00:39:40 2022 -0600 @@ -46,6 +46,16 @@ OCTAVE_BEGIN_NAMESPACE(octave) +bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ ("request_input_enabled"); + return evmgr.request_input_enabled(); +} + +std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ ("request_url"); + return evmgr.request_url(url, param, action, success); +} + static int readline_event_hook (void) { event_manager& evmgr = __get_event_manager__ (); @@ -880,3 +890,27 @@ } OCTAVE_END_NAMESPACE(octave) + +DEFMETHOD (__event_manager_plot_destination__, interp, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (interp.get_event_manager().plot_destination()); +} + +DEFMETHOD (__event_manager_show_static_plot__, interp, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (interp.get_event_manager().show_static_plot(term, content)); +} diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/event-manager.h Fri Dec 30 00:39:40 2022 -0600 @@ -50,6 +50,12 @@ class execution_exception; class symbol_info_list; +enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 +}; + // The methods in this class provide a way to pass signals to the GUI // thread. A GUI that wishes to act on these events should derive // from this class and perform actions in a thread-safe way. In @@ -176,6 +182,18 @@ // confirmation before another action. Could these be reformulated // using the question_dialog action? + bool _request_input_enabled; + virtual std::string request_input (const std::string&) + { + return ""; + } + virtual std::string request_url (const std::string& /*url*/, const std::list& /*param*/, const std::string& /*action*/, bool& /*success*/) { + return ""; + } + + plot_destination_t _plot_destination; + virtual void show_static_plot (const std::string& /*term*/, const std::string& /*content*/) { } + virtual bool confirm_shutdown (void) { return true; } virtual bool prompt_new_edit_file (const std::string& /*file*/) @@ -260,6 +278,8 @@ virtual void clear_history (void) { } + virtual void clear_screen (void) { } + virtual void pre_input_event (void) { } virtual void post_input_event (void) { } @@ -448,6 +468,28 @@ m_instance->update_path_dialog (); } + bool request_input_enabled (void) + { + return enabled () ? m_instance->_request_input_enabled : false; + } + + plot_destination_t plot_destination (void) + { + return enabled () ? m_instance->_plot_destination : TERMINAL_ONLY; + } + + bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + m_instance->show_static_plot (term, content); + return true; + } + else + return false; + } + bool show_preferences (void) { if (enabled ()) @@ -709,6 +751,12 @@ m_instance->clear_history (); } + void clear_screen (void) + { + if (enabled ()) + m_instance->clear_screen (); + } + void pre_input_event (void) { if (enabled ()) @@ -721,6 +769,20 @@ m_instance->post_input_event (); } + std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? m_instance->request_input (prompt) + : std::string (); + } + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? m_instance->request_url (url, param, action, success) + : std::string (); + } + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) { diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/input.cc Fri Dec 30 00:39:40 2022 -0600 @@ -786,7 +786,12 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + event_manager& evmgr = m_interpreter.get_event_manager (); + if (evmgr.request_input_enabled ()) + retval = evmgr.request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1679,3 +1684,32 @@ } OCTAVE_END_NAMESPACE(octave) + +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/interpreter.cc Fri Dec 30 00:39:40 2022 -0600 @@ -61,6 +61,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave.h" @@ -624,6 +625,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (*this, options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } // FIXME: we defer creation of the gh_manager object because it diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Fri Dec 30 00:39:40 2022 -0600 @@ -0,0 +1,102 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" +#include "interpreter.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +namespace octave { + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (new octave_json_link(this)) +{ + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + event_manager& evmgr = interp.get_event_manager (); + evmgr.connect_link (_octave_json_link); + evmgr.install_qt_event_handlers (_octave_json_link); + evmgr.enable (); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link->receive_message(name, jobj); +} + +} // namespace octave diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Fri Dec 30 00:39:40 2022 -0600 @@ -0,0 +1,37 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +namespace octave { + +class interpreter; + +class json_main { +public: + json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + + std::shared_ptr _octave_json_link; +}; + +} // namespace octave + +#endif diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Fri Dec 30 00:39:40 2022 -0600 @@ -0,0 +1,264 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +namespace octave { + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_symbol_info_list(const symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_filter_list(const interpreter_events::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_symbol_info(const symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + + JSON_MAP_T m; + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (size_t i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} + +} // namespace octave diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Fri Dec 30 00:39:40 2022 -0600 @@ -0,0 +1,64 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "syminfo.h" +#include "event-manager.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +namespace octave { + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const interpreter_events::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_symbol_info(const symbol_info element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +} // namespace octave + +#endif diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/module.mk Fri Dec 30 00:39:40 2022 -0600 @@ -46,6 +46,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/latex-text-renderer.h \ %reldir%/load-path.h \ @@ -77,6 +79,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ %reldir%/pr-output.h \ @@ -189,6 +192,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ %reldir%/inv.cc \ @@ -228,6 +233,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/ordqz.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Fri Dec 30 00:39:40 2022 -0600 @@ -0,0 +1,453 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +namespace octave { + +octave_json_link::octave_json_link(json_main* __json_main) + : interpreter_events (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); + +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); + +// return true; +// } + +bool octave_json_link::copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); + +// return message_dialog_queue.dequeue(); +// } + +bool octave_json_link::have_dialogs() const { + // Triggered in "inputdlg" and similar functions to check for dialog support + return true; +} + +std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +void octave_json_link::update_path_dialog(void) { + // Triggered in "rehash" + _publish_message("update-path-dialog", json_util::empty()); +} + +int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::focus_window(const std::string win_name) { + // Triggered in "commandhistory", "commandwindow", "filebrowser", "workspace" + JSON_MAP_T m; + JSON_MAP_SET(m, win_name, string); + _publish_message("focus-window", json_util::from_map(m)); +} + +void octave_json_link::display_exception(const execution_exception& ee, bool beep) { + // Triggered in various places in libinterp + std::ostringstream buf; + ee.display (buf); + std::string ee_str = buf.str(); + JSON_MAP_T m; + JSON_MAP_SET(m, ee_str, string); + JSON_MAP_SET(m, beep, boolean); + _publish_message("display-exception", json_util::from_map(m)); +} + +void octave_json_link::gui_status_update(const std::string& feature, const std::string& status) { + // Triggered in __profiler_enable__ + JSON_MAP_T m; + JSON_MAP_SET(m, feature, string); + JSON_MAP_SET(m, status, string); + _publish_message("gui-status-update", json_util::from_map(m)); +} + +void octave_json_link::update_gui_lexer(void) { + // Triggered in "load_packages_and_dependencies" + _publish_message("update-gui-lexer", json_util::empty()); +} + +void octave_json_link::directory_changed(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + +void octave_json_link::execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +uint8NDArray octave_json_link::get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::set_workspace(bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::pre_input_event(void) { + // noop +} + +void octave_json_link::post_input_event(void) { + // noop +} + +void octave_json_link::enter_debugger_event(const std::string& fcn_name, const std::string& fcn_file_name, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, fcn_name, string); + JSON_MAP_SET(m, fcn_file_name, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } + +void octave_json_link::show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +std::string octave_json_link::gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + +bool octave_json_link::show_documentation(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); + return true; +} + +void octave_json_link::register_documentation (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::unregister_documentation (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} + +void octave_json_link::show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + +} // namespace octave diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Fri Dec 30 00:39:40 2022 -0600 @@ -0,0 +1,267 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "event-manager.h" +#include "json-util.h" +#include "oct-mutex.h" + +class string_vector; + +namespace octave { + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + mutex _mutex; +}; + +class octave_json_link : public interpreter_events +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + // TODO + // void start_gui (bool gui_app = false) override; + // void close_gui (void) override; + + bool have_dialogs (void) const override; + + std::string + question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + void update_path_dialog (void) override; + + void show_preferences (void) override; + + // TODO: + // void apply_preferences (void) override; + + // TODO: + // void show_terminal_window (void) override; + + bool show_documentation (const std::string& file) override; + + // TODO: + // void show_file_browser (void) override; + + // TODO: + // void show_command_history (void) override; + + // TODO: + // void show_workspace (void) override; + + // TODO: + // void show_community_news (int serial) override; + // void show_release_notes (void) override; + + bool edit_file (const std::string& file) override; + + void edit_variable (const std::string& name, const octave_value& val) override; + + std::string request_input (const std::string& prompt) override; + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + void show_static_plot (const std::string& term, const std::string& content) override; + + bool confirm_shutdown (void) override; + + bool prompt_new_edit_file (const std::string& file) override; + + int + debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + uint8NDArray get_named_icon (const std::string& icon_name) override; + + std::string gui_preference (const std::string& key, const std::string& value) override; + + bool copy_image_to_clipboard (const std::string& file) override; + + void focus_window (const std::string win_name) override; + + void execute_command_in_terminal (const std::string& command) override; + + void register_documentation (const std::string& file) override; + + void unregister_documentation (const std::string& file) override; + + // TODO: + // void interpreter_output (const std::string& msg) override; + + void display_exception (const execution_exception& ee, bool beep) override; + + void gui_status_update (const std::string& feature, const std::string& status) override; + + void update_gui_lexer (void) override; + + void directory_changed (const std::string& dir) override; + + void file_remove (const std::string& old_name, const std::string& new_name) override; + + void file_renamed (bool) override; + + void set_workspace (bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) override; + + void clear_workspace (void) override; + + // TODO: + // void update_prompt (const std::string& prompt) override; + + void set_history (const string_vector& hist) override; + + void append_history (const std::string& hist_entry) override; + + void clear_history (void) override; + + void clear_screen (void) override; + + void pre_input_event (void) override; + + void post_input_event (void) override; + + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + + void execute_in_debugger_event (const std::string& file, int line) override; + + void exit_debugger_event (void) override; + + void update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + // TODO: + // void interpreter_interrupted (void) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +} // namespace octave + +#endif diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/syscalls.cc Fri Dec 30 00:39:40 2022 -0600 @@ -150,9 +150,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -475,42 +474,6 @@ return retval; } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (interp.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/sysdep.cc Fri Dec 30 00:39:40 2022 -0600 @@ -75,6 +75,7 @@ #include "defun.h" #include "error.h" #include "errwarn.h" +#include "event-manager.h" #include "input.h" #include "interpreter-private.h" #include "octave.h" @@ -719,7 +720,7 @@ // Read one character from the terminal. -int kbhit (bool wait) +int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -746,13 +747,24 @@ set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + event_manager& evmgr = __get_event_manager__ ("kbhit"); + if (evmgr.request_input_enabled ()) { + std::string line = evmgr.request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). set_interrupt_handler (saved_interrupt_handler, true); @@ -810,6 +822,9 @@ { bool skip_redisplay = true; + octave::event_manager& evmgr = octave::__get_event_manager__ ("clc"); + evmgr.clear_screen(); + command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1244,7 +1259,7 @@ Fdrawnow (interp); - int c = kbhit (args.length () == 0); + int c = kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/sysdep.h Fri Dec 30 00:39:40 2022 -0600 @@ -49,7 +49,7 @@ extern OCTINTERP_API int pclose (FILE *f); -extern OCTINTERP_API int kbhit (bool wait = true); +extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/corefcn/utils.cc Fri Dec 30 00:39:40 2022 -0600 @@ -1559,7 +1559,7 @@ if (do_graphics_events) gh_mgr.process_events (); - c = kbhit (false); + c = kbhit ("press enter to continue", false); } } else diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/dldfcn/__init_gnuplot__.cc --- a/libinterp/dldfcn/__init_gnuplot__.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/dldfcn/__init_gnuplot__.cc Fri Dec 30 00:39:40 2022 -0600 @@ -65,25 +65,6 @@ gnuplot_graphics_toolkit (octave::interpreter& interp) : octave::base_graphics_toolkit ("gnuplot"), m_interpreter (interp) { - static bool warned = false; - - if (! warned) - { - warning_with_id - ("Octave:gnuplot-graphics", - "using the gnuplot graphics toolkit is discouraged\n\ -\n\ -The gnuplot graphics toolkit is not actively maintained and has a number\n\ -of limitations that are unlikely to be fixed. Communication with gnuplot\n\ -uses a one-directional pipe and limited information is passed back to the\n\ -Octave interpreter so most changes made interactively in the plot window\n\ -will not be reflected in the graphics properties managed by Octave. For\n\ -example, if the plot window is closed with a mouse click, Octave will not\n\ -be notified and will not update its internal list of open figure windows.\n\ -The qt toolkit is recommended instead.\n"); - - warned = true; - } } ~gnuplot_graphics_toolkit (void) = default; diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/octave.cc --- a/libinterp/octave.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/octave.cc Fri Dec 30 00:39:40 2022 -0600 @@ -186,6 +186,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -420,6 +430,14 @@ sysdep_init (); } +bool application::link_enabled (void) const +{ + if (m_interpreter) { + event_manager& evmgr = m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; +} + int cli_application::execute (void) { interpreter& interp = create_interpreter (); @@ -442,7 +460,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (application::is_gui_running ()); + return ovl (application::is_link_enabled ()); } /* diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/octave.h --- a/libinterp/octave.h Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/octave.h Fri Dec 30 00:39:40 2022 -0600 @@ -85,6 +85,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -117,6 +119,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -225,6 +229,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; @@ -238,6 +250,7 @@ // both) of them... class interpreter; +class event_manager; // Base class for an Octave application. @@ -287,6 +300,8 @@ virtual bool gui_running (void) const { return false; } virtual void gui_running (bool) { } + bool link_enabled (void) const; + void program_invocation_name (const std::string& nm) { m_program_invocation_name = nm; } @@ -320,6 +335,11 @@ return s_instance ? s_instance->gui_running () : false; } + static bool is_link_enabled (void) + { + return s_instance ? s_instance->link_enabled () : false; + } + // Convenience functions. static bool forced_interactive (void); diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/options.h --- a/libinterp/options.h Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/options.h Fri Dec 30 00:39:40 2022 -0600 @@ -46,17 +46,19 @@ #define IMAGE_PATH_OPTION 7 #define INFO_FILE_OPTION 8 #define INFO_PROG_OPTION 9 -#define LINE_EDITING_OPTION 10 -#define NO_GUI_OPTION 11 -#define NO_GUI_LIBS_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define SERVER_OPTION 18 -#define TEXI_MACROS_FILE_OPTION 19 -#define TRADITIONAL_OPTION 20 +#define JSON_SOCK_OPTION 10 +#define JSON_MAX_LEN_OPTION 11 +#define LINE_EDITING_OPTION 12 +#define NO_GUI_OPTION 13 +#define NO_GUI_LIBS_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define SERVER_OPTION 20 +#define TEXI_MACROS_FILE_OPTION 21 +#define TRADITIONAL_OPTION 22 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, nullptr, TRADITIONAL_OPTION }, @@ -74,6 +76,8 @@ { "info-file", octave_required_arg, nullptr, INFO_FILE_OPTION }, { "info-program", octave_required_arg, nullptr, INFO_PROG_OPTION }, { "interactive", octave_no_arg, nullptr, 'i' }, + { "json-sock", octave_required_arg, nullptr, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, nullptr, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, nullptr, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, nullptr, NO_GUI_OPTION }, { "no-gui-libs", octave_no_arg, nullptr, NO_GUI_LIBS_OPTION }, diff -r 601e7a142a15 -r 4c3d80dd9e65 libinterp/usage.h --- a/libinterp/usage.h Thu Dec 29 16:10:07 2022 +0100 +++ b/libinterp/usage.h Fri Dec 30 00:39:40 2022 -0600 @@ -37,11 +37,11 @@ "octave [-HVWdfhiqvx] [--debug] [--doc-cache-file file] [--echo-commands]\n\ [--eval CODE] [--exec-path path] [--experimental-terminal-widget]\n\ [--gui] [--help] [--image-path path] [--info-file file]\n\ - [--info-program prog] [--interactive] [--line-editing] [--no-gui]\n\ - [--no-history] [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--server] [--silent] [--traditional]\n\ - [--verbose] [--version] [file]"; + [--info-program prog] [--interactive] [--json-sock] [--json-max-len] \n\ + [--line-editing] [--no-gui] [--no-history] [--no-init-file] \n\ + [--no-init-path] [--no-line-editing] [--no-site-file] \n\ + [--no-window-system] [--norc] [-p path] [--path path] [--persist] \n\ + [--server] [--silent] [--traditional] [--verbose] [--version] [file]"; // Usage message with extra help. @@ -69,6 +69,8 @@ --info-file FILE Use top-level info file FILE.\n\ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 601e7a142a15 -r 4c3d80dd9e65 liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/liboctave/util/oct-mutex.cc Fri Dec 30 00:39:40 2022 -0600 @@ -58,6 +58,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -68,11 +80,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -90,8 +104,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -123,11 +148,18 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&m_pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&m_pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -145,8 +177,19 @@ return (pthread_mutex_trylock (&m_pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &m_pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t m_pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 601e7a142a15 -r 4c3d80dd9e65 liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Thu Dec 29 16:10:07 2022 +0100 +++ b/liboctave/util/oct-mutex.h Fri Dec 30 00:39:40 2022 -0600 @@ -50,6 +50,10 @@ virtual void unlock (void); virtual bool try_lock (void); + + virtual void cond_wait (void); + + virtual void cond_signal (void); }; class @@ -80,6 +84,16 @@ return m_rep->try_lock (); } + void cond_wait (void) + { + m_rep->cond_wait (); + } + + void cond_signal (void) + { + m_rep->cond_signal (); + } + protected: std::shared_ptr m_rep; }; diff -r 601e7a142a15 -r 4c3d80dd9e65 liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Thu Dec 29 16:10:07 2022 +0100 +++ b/liboctave/util/url-transfer.cc Fri Dec 30 00:39:40 2022 -0600 @@ -31,6 +31,7 @@ #include #include +#include "base64-wrappers.h" #include "dir-ops.h" #include "file-ops.h" #include "file-stat.h" @@ -48,6 +49,10 @@ OCTAVE_BEGIN_NAMESPACE(octave) +// Forward declaration for event_manager +extern bool __event_manager_request_input_enabled__(); +extern std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success); + base_url_transfer::base_url_transfer (void) : m_host_or_url (), m_valid (false), m_ftp (false), m_ascii_mode (false), m_ok (true), m_errmsg (), @@ -228,6 +233,86 @@ return file_list; } + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + m_valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + m_valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + m_valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform_action (param, "get"); + } + + void http_post (const Array& param) { + perform_action (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform_action (param, action); + } + +private: + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (__event_manager_request_input_enabled__()) { + bool success; + std::string result = __event_manager_request_url__(url, paramList, action, success); + if (success) { + process_success(result); + } else { + m_ok = false; + m_errmsg = result; + } + } else { + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + std::ptrdiff_t outlen; + bool b64_ok = octave_base64_decode_alloc_wrapper(inc, result.length(), &out, &outlen); + if (!b64_ok) { + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; + } else { + m_curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + + #if defined (HAVE_CURL) static int @@ -918,17 +1003,30 @@ # define REP_CLASS base_url_transfer #endif -url_transfer::url_transfer (void) : m_rep (new REP_CLASS ()) -{ } +url_transfer::url_transfer (void) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new REP_CLASS()); + } +} url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : m_rep (new REP_CLASS (host, user, passwd, os)) -{ } + const std::string& passwd, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new REP_CLASS(host, user, passwd, os)); + } +} -url_transfer::url_transfer (const std::string& url, std::ostream& os) - : m_rep (new REP_CLASS (url, os)) -{ } +url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new REP_CLASS(url, os)); + } +} #undef REP_CLASS diff -r 601e7a142a15 -r 4c3d80dd9e65 scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Thu Dec 29 16:10:07 2022 +0100 +++ b/scripts/help/__unimplemented__.m Fri Dec 30 00:39:40 2022 -0600 @@ -45,7 +45,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -524,6 +547,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -566,13 +590,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run 'pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 601e7a142a15 -r 4c3d80dd9e65 scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Thu Dec 29 16:10:07 2022 +0100 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Fri Dec 30 00:39:40 2022 -0600 @@ -32,9 +32,84 @@ if (nargin < 1 || nargin == 2) print_usage (); - endif + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __event_manager_plot_destination__ () == 0 || + __event_manager_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif - if (nargin >= 3 && nargin <= 4) + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __event_manager_plot_destination__ () == 1 || + __event_manager_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __event_manager_show_static_plot__ (term, a); + endif + endif + endif + + endif +endfunction + +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -70,44 +145,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: back-octave/oo-changesets/421-de16dd99ab0e.hg.txt ================================================ # HG changeset patch # User Octave Online Team # Date 1672385388 21600 # Fri Dec 30 01:29:48 2022 -0600 # Branch oo-7.4 # Node ID de16dd99ab0ec3baee489d7187cfd184df64aca6 # Parent 41ff58a98cd0078da6f5de9cfec0b9064dedbafa Moving URL event manager to libinterp, and other minor changes for 7.4 diff -r 41ff58a98cd0 -r de16dd99ab0e libinterp/corefcn/event-manager-url.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/event-manager-url.cc Fri Dec 30 01:29:48 2022 -0600 @@ -0,0 +1,127 @@ +#if defined (HAVE_CONFIG_H) +# include "config.h" +#endif + +#include + +#include "event-manager.h" +#include "url-transfer.h" +#include "interpreter-private.h" +#include "base64-wrappers.h" + +OCTAVE_BEGIN_NAMESPACE(octave) + +bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ (); + return evmgr.request_input_enabled(); +} + +std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ (); + return evmgr.request_url(url, param, action, success); +} + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + m_valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + m_valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + m_valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform_action (param, "get"); + } + + void http_post (const Array& param) { + perform_action (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform_action (param, action); + } + +private: + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (__event_manager_request_input_enabled__()) { + bool success; + std::string result = __event_manager_request_url__(url, paramList, action, success); + if (success) { + process_success(result); + } else { + m_ok = false; + m_errmsg = result; + } + } else { + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + std::ptrdiff_t outlen; + bool b64_ok = octave_base64_decode_alloc_wrapper(inc, result.length(), &out, &outlen); + if (!b64_ok) { + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; + } else { + m_curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + +url_transfer::url_transfer (void) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new base_url_transfer()); + } +} + +url_transfer::url_transfer (const std::string& host, const std::string& user, + const std::string& passwd, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new base_url_transfer(host, user, passwd, os)); + } +} + +url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new base_url_transfer(url, os)); + } +} + +OCTAVE_END_NAMESPACE(octave) diff -r 41ff58a98cd0 -r de16dd99ab0e libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Fri Dec 30 00:39:40 2022 -0600 +++ b/libinterp/corefcn/event-manager.cc Fri Dec 30 01:29:48 2022 -0600 @@ -46,16 +46,6 @@ OCTAVE_BEGIN_NAMESPACE(octave) -bool __event_manager_request_input_enabled__() { - event_manager& evmgr = __get_event_manager__ ("request_input_enabled"); - return evmgr.request_input_enabled(); -} - -std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { - event_manager& evmgr = __get_event_manager__ ("request_url"); - return evmgr.request_url(url, param, action, success); -} - static int readline_event_hook (void) { event_manager& evmgr = __get_event_manager__ (); @@ -889,8 +879,6 @@ return ovl (); } -OCTAVE_END_NAMESPACE(octave) - DEFMETHOD (__event_manager_plot_destination__, interp, , , doc: /* -*- texinfo -*- @deftypefn {} {} __event_manager_plot_destination__ () @@ -914,3 +902,5 @@ std::string content = args(1).string_value(); return ovl (interp.get_event_manager().show_static_plot(term, content)); } + +OCTAVE_END_NAMESPACE(octave) diff -r 41ff58a98cd0 -r de16dd99ab0e libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Fri Dec 30 00:39:40 2022 -0600 +++ b/libinterp/corefcn/input.cc Fri Dec 30 01:29:48 2022 -0600 @@ -1683,8 +1683,6 @@ return input_sys.auto_repeat_debug_command (args, nargout); } -OCTAVE_END_NAMESPACE(octave) - DEFUN (current_command_number, args, , doc: /* -*- texinfo -*- @deftypefn {} {@var{val} =} current_command_number () @@ -1713,3 +1711,5 @@ return ovl(n); } } + +OCTAVE_END_NAMESPACE(octave) diff -r 41ff58a98cd0 -r de16dd99ab0e libinterp/corefcn/json-util.cc --- a/libinterp/corefcn/json-util.cc Fri Dec 30 00:39:40 2022 -0600 +++ b/libinterp/corefcn/json-util.cc Fri Dec 30 01:29:48 2022 -0600 @@ -245,7 +245,7 @@ } json_tokener_free(tok); - delete buf; + delete[] buf; } void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { diff -r 41ff58a98cd0 -r de16dd99ab0e libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Fri Dec 30 00:39:40 2022 -0600 +++ b/libinterp/corefcn/module.mk Fri Dec 30 01:29:48 2022 -0600 @@ -166,6 +166,7 @@ %reldir%/error.cc \ %reldir%/errwarn.cc \ %reldir%/event-manager.cc \ + %reldir%/event-manager-url.cc \ %reldir%/event-queue.cc \ %reldir%/fcn-info.cc \ %reldir%/fft.cc \ diff -r 41ff58a98cd0 -r de16dd99ab0e libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Fri Dec 30 00:39:40 2022 -0600 +++ b/libinterp/corefcn/sysdep.cc Fri Dec 30 01:29:48 2022 -0600 @@ -748,7 +748,7 @@ set_interrupt_handler (saved_interrupt_handler, false); int c; - event_manager& evmgr = __get_event_manager__ ("kbhit"); + event_manager& evmgr = __get_event_manager__ (); if (evmgr.request_input_enabled ()) { std::string line = evmgr.request_input (prompt); if (line.length() >= 1) { @@ -822,7 +822,7 @@ { bool skip_redisplay = true; - octave::event_manager& evmgr = octave::__get_event_manager__ ("clc"); + octave::event_manager& evmgr = octave::__get_event_manager__ (); evmgr.clear_screen(); command_editor::clear_screen (skip_redisplay); diff -r 41ff58a98cd0 -r de16dd99ab0e liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Fri Dec 30 00:39:40 2022 -0600 +++ b/liboctave/util/url-transfer.cc Fri Dec 30 01:29:48 2022 -0600 @@ -49,10 +49,6 @@ OCTAVE_BEGIN_NAMESPACE(octave) -// Forward declaration for event_manager -extern bool __event_manager_request_input_enabled__(); -extern std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success); - base_url_transfer::base_url_transfer (void) : m_host_or_url (), m_valid (false), m_ftp (false), m_ascii_mode (false), m_ok (true), m_errmsg (), @@ -234,84 +230,6 @@ } -class link_transfer : public base_url_transfer -{ -public: - - link_transfer (void) - : base_url_transfer () { - m_valid = true; - } - - link_transfer (const std::string& host, const std::string& user_arg, - const std::string& passwd, std::ostream& os) - : base_url_transfer (host, user_arg, passwd, os) { - m_valid = true; - // url = "ftp://" + host; - } - - link_transfer (const std::string& url_str, std::ostream& os) - : base_url_transfer (url_str, os) { - m_valid = true; - } - - ~link_transfer (void) {} - - void http_get (const Array& param) { - perform_action (param, "get"); - } - - void http_post (const Array& param) { - perform_action (param, "post"); - } - - void http_action (const Array& param, const std::string& action) { - perform_action (param, action); - } - -private: - void perform_action(const Array& param, const std::string& action) { - std::string url = m_host_or_url; - - // Convert from Array to std::list - std::list paramList; - for (int i = 0; i < param.numel(); i ++) { - std::string value = param(i); - paramList.push_back(value); - } - - if (__event_manager_request_input_enabled__()) { - bool success; - std::string result = __event_manager_request_url__(url, paramList, action, success); - if (success) { - process_success(result); - } else { - m_ok = false; - m_errmsg = result; - } - } else { - m_ok = false; - m_errmsg = "octave_link not connected for link_transfer"; - } - } - - void process_success(const std::string& result) { - // If success, the result is returned as a base64 string, and we need to decode it. - // Use the base64 implementation from gnulib, which is already an Octave dependency. - const char *inc = &(result[0]); - char *out; - std::ptrdiff_t outlen; - bool b64_ok = octave_base64_decode_alloc_wrapper(inc, result.length(), &out, &outlen); - if (!b64_ok) { - m_ok = false; - m_errmsg = "failed decoding base64 from octave_link"; - } else { - m_curr_ostream->write(out, outlen); - ::free(out); - } - } -}; - #if defined (HAVE_CURL) @@ -1003,31 +921,6 @@ # define REP_CLASS base_url_transfer #endif -url_transfer::url_transfer (void) { - if (__event_manager_request_input_enabled__()) { - m_rep.reset(new link_transfer()); - } else { - m_rep.reset(new REP_CLASS()); - } -} - -url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) { - if (__event_manager_request_input_enabled__()) { - m_rep.reset(new link_transfer(host, user, passwd, os)); - } else { - m_rep.reset(new REP_CLASS(host, user, passwd, os)); - } -} - -url_transfer::url_transfer (const std::string& url, std::ostream& os) { - if (__event_manager_request_input_enabled__()) { - m_rep.reset(new link_transfer(url, os)); - } else { - m_rep.reset(new REP_CLASS(url, os)); - } -} - #undef REP_CLASS OCTAVE_END_NAMESPACE(octave) ================================================ FILE: back-octave/oo-changesets/422-de16dd99ab0e.hg.txt ================================================ # HG changeset patch # User Octave Online Team # Date 1673173830 21600 # Sun Jan 08 04:30:30 2023 -0600 # Branch oo-7.4 # Node ID a602982ec42d2e26dd5f1b127afb64011033348d # Parent de16dd99ab0ec3baee489d7187cfd184df64aca6 Redirect warnings to new display-warning message diff -r de16dd99ab0e -r a602982ec42d libinterp/corefcn/error.cc --- a/libinterp/corefcn/error.cc Fri Dec 30 01:29:48 2022 -0600 +++ b/libinterp/corefcn/error.cc Sun Jan 08 04:30:30 2023 -0600 @@ -533,7 +533,9 @@ if (! quiet_warning ()) { octave_diary << msg_string; - std::cerr << msg_string; + //std::cerr << msg_string; + event_manager& evmgr = m_interpreter.get_event_manager (); + evmgr.display_warning (id, name, base_msg, msg_string); if (! fmt_suppresses_backtrace && in_user_code && backtrace_on_warning () diff -r de16dd99ab0e -r a602982ec42d libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Fri Dec 30 01:29:48 2022 -0600 +++ b/libinterp/corefcn/event-manager.cc Sun Jan 08 04:30:30 2023 -0600 @@ -64,6 +64,11 @@ ee.display (std::cerr); } +void interpreter_events::display_warning (const std::string&, const std::string&, const std::string&, const std::string& formatted) +{ + std::cerr << formatted; +} + event_manager::event_manager (interpreter& interp) : m_event_queue_mutex (new mutex ()), m_gui_event_queue (), m_debugging (false), m_link_enabled (true), diff -r de16dd99ab0e -r a602982ec42d libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Fri Dec 30 01:29:48 2022 -0600 +++ b/libinterp/corefcn/event-manager.h Sun Jan 08 04:30:30 2023 -0600 @@ -246,6 +246,8 @@ virtual void display_exception (const execution_exception& ee, bool beep); + virtual void display_warning (const std::string& id, const std::string& name, const std::string& message, const std::string& formatted); + virtual void gui_status_update (const std::string& /*feature*/, const std::string& /*status*/) { } @@ -667,6 +669,17 @@ return false; } + bool display_warning (const std::string& id, const std::string& name, const std::string& message, const std::string& formatted) + { + if (enabled ()) + { + m_instance->display_warning (id, name, message, formatted); + return true; + } + else + return false; + } + bool gui_status_update (const std::string& feature, const std::string& status) { diff -r de16dd99ab0e -r a602982ec42d libinterp/corefcn/octave-json-link.cc --- a/libinterp/corefcn/octave-json-link.cc Fri Dec 30 01:29:48 2022 -0600 +++ b/libinterp/corefcn/octave-json-link.cc Sun Jan 08 04:30:30 2023 -0600 @@ -221,6 +221,16 @@ _publish_message("display-exception", json_util::from_map(m)); } +void octave_json_link::display_warning(const std::string& id, const std::string& name, const std::string& message, const std::string& formatted) { + // Redirected warnings from std::cerr + JSON_MAP_T m; + JSON_MAP_SET(m, id, string); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, message, string); + JSON_MAP_SET(m, formatted, string); + _publish_message("display-warning", json_util::from_map(m)); +} + void octave_json_link::gui_status_update(const std::string& feature, const std::string& status) { // Triggered in __profiler_enable__ JSON_MAP_T m; diff -r de16dd99ab0e -r a602982ec42d libinterp/corefcn/octave-json-link.h --- a/libinterp/corefcn/octave-json-link.h Fri Dec 30 01:29:48 2022 -0600 +++ b/libinterp/corefcn/octave-json-link.h Sun Jan 08 04:30:30 2023 -0600 @@ -157,6 +157,8 @@ void display_exception (const execution_exception& ee, bool beep) override; + void display_warning (const std::string& id, const std::string& name, const std::string& message, const std::string& formatted) override; + void gui_status_update (const std::string& feature, const std::string& status) override; void update_gui_lexer (void) override; ================================================ FILE: back-octave/oo-changesets/430-d2250ae9bddd.hg.patch ================================================ # HG changeset patch # User username = Octave Online Team # Date 1703965894 21600 # Sat Dec 30 13:51:34 2023 -0600 # Branch oo-8.4 # Node ID d2250ae9bddde54dfc2ec9c772ab997376b1c307 # Parent 78c13a2594f359474f28a93e43b288c9b40afa2c # Parent 44753c71594bb69cf144106cd06aa4246c703e00 Merge oo-7.4 into oo-8.4 diff -r 78c13a2594f3 -r d2250ae9bddd configure.ac --- a/configure.ac Sun Nov 05 12:45:40 2023 -0500 +++ b/configure.ac Sat Dec 30 13:51:34 2023 -0600 @@ -2983,7 +2983,7 @@ AC_SUBST(LIBOCTAVE_LINK_DEPS) AC_SUBST(LIBOCTAVE_LINK_OPTS) -LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $JAVA_LIBS $LAPACK_LIBS" +LIBOCTINTERP_LINK_DEPS="$FT2_LIBS $HDF5_LIBS $MAGICK_LIBS $Z_LIBS $SPARSE_XLIBS $FFTW_XLIBS $OPENGL_LIBS $FONTCONFIG_LIBS $FREETYPE_LIBS $X11_LIBS $CARBON_LIBS $GL2PS_LIBS $JAVA_LIBS $LAPACK_LIBS -ljson-c" LIBOCTINTERP_LINK_OPTS="$FT2_LDFLAGS $HDF5_LDFLAGS $MAGICK_LDFLAGS $Z_LDFLAGS $SPARSE_XLDFLAGS $FFTW_XLDFLAGS" diff -r 78c13a2594f3 -r d2250ae9bddd libgui/src/qt-interpreter-events.cc --- a/libgui/src/qt-interpreter-events.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libgui/src/qt-interpreter-events.cc Sat Dec 30 13:51:34 2023 -0600 @@ -309,6 +309,21 @@ emit edit_variable_signal (QString::fromStdString (expr), val); } +void qt_interpreter_events::show_static_plot (const std::string&, const std::string&) +{ + return; +} + +std::string qt_interpreter_events::request_input (const std::string&) +{ + return {}; +} + +std::string qt_interpreter_events::request_url (const std::string&, const std::list&, const std::string&, bool&) +{ + return {}; +} + bool qt_interpreter_events::confirm_shutdown (void) { QMutexLocker autolock (&m_mutex); @@ -604,6 +619,9 @@ emit clear_history_signal (); } +void qt_interpreter_events::do_clear_screen (void) +{ } + void qt_interpreter_events::pre_input_event (void) { } diff -r 78c13a2594f3 -r d2250ae9bddd libgui/src/qt-interpreter-events.h --- a/libgui/src/qt-interpreter-events.h Sun Nov 05 12:45:40 2023 -0500 +++ b/libgui/src/qt-interpreter-events.h Sat Dec 30 13:51:34 2023 -0600 @@ -136,6 +136,12 @@ void edit_variable (const std::string& name, const octave_value& val); + void show_static_plot (const std::string& term, const std::string& content); + + std::string request_input (const std::string&); + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success); + bool confirm_shutdown (void); bool prompt_new_edit_file (const std::string& file); @@ -190,6 +196,8 @@ void clear_history (void); + void clear_screen (void); + void pre_input_event (void); void post_input_event (void); diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/error.cc --- a/libinterp/corefcn/error.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/error.cc Sat Dec 30 13:51:34 2023 -0600 @@ -533,7 +533,9 @@ if (! quiet_warning ()) { octave_diary << msg_string; - std::cerr << msg_string; + //std::cerr << msg_string; + event_manager& evmgr = m_interpreter.get_event_manager (); + evmgr.display_warning (id, name, base_msg, msg_string); if (! fmt_suppresses_backtrace && in_user_code && backtrace_on_warning () diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/event-manager-url.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/event-manager-url.cc Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,127 @@ +#if defined (HAVE_CONFIG_H) +# include "config.h" +#endif + +#include + +#include "event-manager.h" +#include "url-transfer.h" +#include "interpreter-private.h" +#include "base64-wrappers.h" + +OCTAVE_BEGIN_NAMESPACE(octave) + +bool __event_manager_request_input_enabled__() { + event_manager& evmgr = __get_event_manager__ (); + return evmgr.request_input_enabled(); +} + +std::string __event_manager_request_url__(const std::string& url, const std::list& param, const std::string& action, bool& success) { + event_manager& evmgr = __get_event_manager__ (); + return evmgr.request_url(url, param, action, success); +} + +class link_transfer : public base_url_transfer +{ +public: + + link_transfer (void) + : base_url_transfer () { + m_valid = true; + } + + link_transfer (const std::string& host, const std::string& user_arg, + const std::string& passwd, std::ostream& os) + : base_url_transfer (host, user_arg, passwd, os) { + m_valid = true; + // url = "ftp://" + host; + } + + link_transfer (const std::string& url_str, std::ostream& os) + : base_url_transfer (url_str, os) { + m_valid = true; + } + + ~link_transfer (void) {} + + void http_get (const Array& param) { + perform_action (param, "get"); + } + + void http_post (const Array& param) { + perform_action (param, "post"); + } + + void http_action (const Array& param, const std::string& action) { + perform_action (param, action); + } + +private: + void perform_action(const Array& param, const std::string& action) { + std::string url = m_host_or_url; + + // Convert from Array to std::list + std::list paramList; + for (int i = 0; i < param.numel(); i ++) { + std::string value = param(i); + paramList.push_back(value); + } + + if (__event_manager_request_input_enabled__()) { + bool success; + std::string result = __event_manager_request_url__(url, paramList, action, success); + if (success) { + process_success(result); + } else { + m_ok = false; + m_errmsg = result; + } + } else { + m_ok = false; + m_errmsg = "octave_link not connected for link_transfer"; + } + } + + void process_success(const std::string& result) { + // If success, the result is returned as a base64 string, and we need to decode it. + // Use the base64 implementation from gnulib, which is already an Octave dependency. + const char *inc = &(result[0]); + char *out; + std::ptrdiff_t outlen; + bool b64_ok = octave_base64_decode_alloc_wrapper(inc, result.length(), &out, &outlen); + if (!b64_ok) { + m_ok = false; + m_errmsg = "failed decoding base64 from octave_link"; + } else { + m_curr_ostream->write(out, outlen); + ::free(out); + } + } +}; + +url_transfer::url_transfer (void) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer()); + } else { + m_rep.reset(new base_url_transfer()); + } +} + +url_transfer::url_transfer (const std::string& host, const std::string& user, + const std::string& passwd, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(host, user, passwd, os)); + } else { + m_rep.reset(new base_url_transfer(host, user, passwd, os)); + } +} + +url_transfer::url_transfer (const std::string& url, std::ostream& os) { + if (__event_manager_request_input_enabled__()) { + m_rep.reset(new link_transfer(url, os)); + } else { + m_rep.reset(new base_url_transfer(url, os)); + } +} + +OCTAVE_END_NAMESPACE(octave) diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/event-manager.cc --- a/libinterp/corefcn/event-manager.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/event-manager.cc Sat Dec 30 13:51:34 2023 -0600 @@ -64,6 +64,11 @@ ee.display (std::cerr); } +void interpreter_events::display_warning (const std::string&, const std::string&, const std::string&, const std::string& formatted) +{ + std::cerr << formatted; +} + event_manager::event_manager (interpreter& interp) : m_event_queue_mutex (new mutex ()), m_gui_event_queue (), m_debugging (false), m_link_enabled (true), @@ -879,4 +884,28 @@ return ovl (); } +DEFMETHOD (__event_manager_plot_destination__, interp, , , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_plot_destination__ () +Undocumented internal function. +@end deftypefn*/) +{ + return ovl (interp.get_event_manager().plot_destination()); +} + +DEFMETHOD (__event_manager_show_static_plot__, interp, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {} __event_manager_show_static_plot__ (@var{term}, @var{content}) +Undocumented internal function. +@end deftypefn*/) +{ + if (args.length () != 2) { + return ovl (); + } + + std::string term = args(0).string_value(); + std::string content = args(1).string_value(); + return ovl (interp.get_event_manager().show_static_plot(term, content)); +} + OCTAVE_END_NAMESPACE(octave) diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/event-manager.h --- a/libinterp/corefcn/event-manager.h Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/event-manager.h Sat Dec 30 13:51:34 2023 -0600 @@ -50,6 +50,12 @@ class execution_exception; class symbol_info_list; +enum plot_destination_t { + TERMINAL_ONLY = 0, + STATIC_ONLY = 1, + TERMINAL_AND_STATIC = 2 +}; + // The methods in this class provide a way to pass signals to the GUI // thread. A GUI that wishes to act on these events should derive // from this class and perform actions in a thread-safe way. In @@ -176,6 +182,18 @@ // confirmation before another action. Could these be reformulated // using the question_dialog action? + bool _request_input_enabled; + virtual std::string request_input (const std::string&) + { + return ""; + } + virtual std::string request_url (const std::string& /*url*/, const std::list& /*param*/, const std::string& /*action*/, bool& /*success*/) { + return ""; + } + + plot_destination_t _plot_destination; + virtual void show_static_plot (const std::string& /*term*/, const std::string& /*content*/) { } + virtual bool confirm_shutdown (void) { return true; } virtual bool prompt_new_edit_file (const std::string& /*file*/) @@ -228,6 +246,8 @@ virtual void display_exception (const execution_exception& ee, bool beep); + virtual void display_warning (const std::string& id, const std::string& name, const std::string& message, const std::string& formatted); + virtual void gui_status_update (const std::string& /*feature*/, const std::string& /*status*/) { } @@ -260,6 +280,8 @@ virtual void clear_history (void) { } + virtual void clear_screen (void) { } + virtual void pre_input_event (void) { } virtual void post_input_event (void) { } @@ -448,6 +470,28 @@ m_instance->update_path_dialog (); } + bool request_input_enabled (void) + { + return enabled () ? m_instance->_request_input_enabled : false; + } + + plot_destination_t plot_destination (void) + { + return enabled () ? m_instance->_plot_destination : TERMINAL_ONLY; + } + + bool + show_static_plot (const std::string& term, const std::string& content) + { + if (enabled ()) + { + m_instance->show_static_plot (term, content); + return true; + } + else + return false; + } + bool show_preferences (void) { if (enabled ()) @@ -625,6 +669,17 @@ return false; } + bool display_warning (const std::string& id, const std::string& name, const std::string& message, const std::string& formatted) + { + if (enabled ()) + { + m_instance->display_warning (id, name, message, formatted); + return true; + } + else + return false; + } + bool gui_status_update (const std::string& feature, const std::string& status) { @@ -709,6 +764,12 @@ m_instance->clear_history (); } + void clear_screen (void) + { + if (enabled ()) + m_instance->clear_screen (); + } + void pre_input_event (void) { if (enabled ()) @@ -721,6 +782,20 @@ m_instance->post_input_event (); } + std::string request_input (const std::string& prompt) + { + return request_input_enabled () + ? m_instance->request_input (prompt) + : std::string (); + } + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) + { + return request_input_enabled () + ? m_instance->request_url (url, param, action, success) + : std::string (); + } + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) { diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/input.cc --- a/libinterp/corefcn/input.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/input.cc Sat Dec 30 13:51:34 2023 -0600 @@ -786,7 +786,12 @@ eof = false; - std::string retval = command_editor::readline (s, eof); + std::string retval; + event_manager& evmgr = m_interpreter.get_event_manager (); + if (evmgr.request_input_enabled ()) + retval = evmgr.request_input (s); + else + retval = command_editor::readline (s, eof); if (! eof && retval.empty ()) retval = "\n"; @@ -1678,4 +1683,33 @@ return input_sys.auto_repeat_debug_command (args, nargout); } +DEFUN (current_command_number, args, , + doc: /* -*- texinfo -*- +@deftypefn {} {@var{val} =} current_command_number () +@deftypefnx {} {@var{old_val} =} current_command_number (@var{new_val}) +Sets the current command number, which appears in the prompt string. +For example, if the prompt says "octave:1>", then the current command +number is 1. + +This is a custom function in Octave Online. + +@example +current_command_number(1) +@end example +@end deftypefn */) +{ + int nargin = args.length (); + if (nargin == 0) { + int n = octave::command_editor::current_command_number(); + return ovl(n); + } else if (nargin > 1) { + print_usage (); + return ovl(); + } else { + int n = args(0).int_value (); + octave::command_editor::reset_current_command_number(n); + return ovl(n); + } +} + OCTAVE_END_NAMESPACE(octave) diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/interpreter.cc --- a/libinterp/corefcn/interpreter.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/interpreter.cc Sat Dec 30 13:51:34 2023 -0600 @@ -61,6 +61,7 @@ #include "input.h" #include "interpreter-private.h" #include "interpreter.h" +#include "json-main.h" #include "load-path.h" #include "load-save.h" #include "octave.h" @@ -624,6 +625,11 @@ std::string texi_macros_file = options.texi_macros_file (); if (! texi_macros_file.empty ()) Ftexi_macros_file (*this, octave_value (texi_macros_file)); + + if (!options.json_sock_path().empty ()) { + static json_main _json_main (*this, options.json_sock_path(), options.json_max_message_length()); + _json_main.run_loop_on_new_thread(); + } } // FIXME: we defer creation of the gh_manager object because it diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/json-main.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.cc Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,102 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "json-main.h" +#include "interpreter.h" + +#include +#include +#include +#include + + +// Analog of main-window.cc +// TODO: Think more about concurrency and null pointer exceptions + +namespace octave { + +void* run_loop_pthread(void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->run_loop(); + return NULL; +} + +void json_object_cb(std::string name, JSON_OBJECT_T jobj, void* arg) { + json_main* _json_main = static_cast(arg); + _json_main->process_json_object(name, jobj); +} + +json_main::json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length) + : _json_sock_path (json_sock_path), + _max_message_length (max_message_length), + _loop_thread_active (false), + _octave_json_link (new octave_json_link(this)) +{ + // Enable the octave_json_link instance + // Note: this passes ownership to octave_link + event_manager& evmgr = interp.get_event_manager (); + evmgr.connect_link (_octave_json_link); + evmgr.install_qt_event_handlers (_octave_json_link); + evmgr.enable (); + + // Open UNIX socket file descriptor + sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(&addr.sun_path, _json_sock_path.c_str(), sizeof(addr.sun_path)-1); + connect( + sockfd, + reinterpret_cast(&addr), + sizeof(addr)); +} + +json_main::~json_main(void) { + close(sockfd); + + // TODO: Stop the _loop_thread +} + +void json_main::publish_message(const std::string& name, JSON_OBJECT_T jobj) { + std::string jstr = json_util::to_message(name, jobj); + + // Do not send any messages over the socket that exceed the user-specified max length. Instead, send an error message. If max_length is 0 (default), do not suppress any messages. + // In stress testing, Node may be able to handle as much as 10-20 MB, but I'd prefer to stay on the safe side. Before this safeguard was implemented, fewer than 5% of plots exceeded 1 MB. + int length = jstr.length(); + int max_length = _max_message_length; + if (max_length > 0 && length > max_length) { + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, length, int); + JSON_MAP_SET(m, max_length, int); + jstr = json_util::to_message("message-too-long", json_util::from_map(m)); + } + + send(sockfd, jstr.c_str(), jstr.length(), 0); +} + +void json_main::run_loop_on_new_thread(void) { + if (_loop_thread_active) + perror("won't run JSON socket loop multiple times"); + _loop_thread_active = true; + + pthread_create( + &_loop_thread, + NULL, + run_loop_pthread, + static_cast(this)); +} + +void json_main::run_loop(void) { + json_util::read_stream( + sockfd, + json_object_cb, + static_cast(this)); +} + +void json_main::process_json_object(std::string name, JSON_OBJECT_T jobj) { + _octave_json_link->receive_message(name, jobj); +} + +} // namespace octave diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/json-main.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-main.h Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,37 @@ +#ifndef json_main_h +#define json_main_h + +#include +#include +#include + +#include "octave-json-link.h" +#include "json-util.h" + +namespace octave { + +class interpreter; + +class json_main { +public: + json_main(interpreter& interp, const std::string& json_sock_path, int max_message_length); + ~json_main(void); + + void publish_message(const std::string& name, JSON_OBJECT_T jobj); + void run_loop_on_new_thread(void); + void run_loop(void); + void process_json_object(std::string name, JSON_OBJECT_T jobj); + +private: + std::string _json_sock_path; + int _max_message_length; + int sockfd; + bool _loop_thread_active; + pthread_t _loop_thread; + + std::shared_ptr _octave_json_link; +}; + +} // namespace octave + +#endif diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/json-util.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.cc Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,264 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "str-vec.h" + +#include "json-util.h" + +namespace octave { + +JSON_OBJECT_T json_util::from_string(const std::string& str) { + // Note: the string is not necesarilly valid UTF-8. The consumers of this stream must be able to handle that situation and substitute replacement characters, etc., where necessary. + return json_object_new_string_len(str.c_str(), str.length()); +} + +JSON_OBJECT_T json_util::from_int(int i) { + return json_object_new_int(i); +} + +JSON_OBJECT_T json_util::from_float(float flt) { + return json_object_new_double(flt); +} + +JSON_OBJECT_T json_util::from_boolean(bool b) { + return json_object_new_boolean(b); +} + +JSON_OBJECT_T json_util::empty() { + return json_object_new_object(); +} + +template +JSON_OBJECT_T json_object_from_list(const std::list& list, JSON_OBJECT_T (*convert)(T)) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, convert(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_string_list(const std::list& list) { + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_string_vector(const string_vector& vect) { + // TODO: Make sure this function does what it's supposed to do + std::list list; + for (int i = 0; i < vect.numel(); ++i) { + list.push_back(vect[i]); + } + + return json_object_from_list(list, json_util::from_value_string); +} + +JSON_OBJECT_T json_util::from_int_list(const std::list& list) { + return json_object_from_list(list, json_util::from_int); +} + +JSON_OBJECT_T json_util::from_float_list(const std::list& list) { + return json_object_from_list(list, json_util::from_float); +} + +JSON_OBJECT_T json_util::from_symbol_info_list(const symbol_info_list& list) { + JSON_OBJECT_T jobj = json_object_new_array(); + for ( + auto it = list.begin(); + it != list.end(); + ++it + ){ + json_object_array_add(jobj, json_util::from_symbol_info(*it)); + } + return jobj; +} + +JSON_OBJECT_T json_util::from_filter_list(const interpreter_events::filter_list& list) { + return json_object_from_list(list, json_util::from_pair); +} + +JSON_OBJECT_T json_util::from_value_string(const std::string str) { + return json_util::from_string(str); +} + +JSON_OBJECT_T json_util::from_symbol_info(const symbol_info element) { + octave_value val = element.value(); + + std::string dims_str = val.get_dims_str(); + + std::ostringstream display_str; + val.short_disp(display_str); + + JSON_MAP_T m; + // m["scope"] = json_util::from_int(element.scope()); + m["symbol"] = json_util::from_string(element.name()); + m["class_name"] = json_util::from_string(val.class_name()); + m["dimension"] = json_util::from_string(dims_str); + m["value"] = json_util::from_string(display_str.str()); + m["complex_flag"] = json_util::from_boolean(element.is_complex()); + return json_util::from_map(m); +} + +JSON_OBJECT_T json_util::from_pair(std::pair pair) { + JSON_OBJECT_T jobj = json_object_new_array(); + json_object_array_add(jobj, json_util::from_string(pair.first.c_str())); + json_object_array_add(jobj, json_util::from_string(pair.second.c_str())); + return jobj; +} + +JSON_OBJECT_T json_util::from_map(JSON_MAP_T m) { + JSON_OBJECT_T jobj = json_object_new_object(); + for( + std::map::iterator it = m.begin(); + it != m.end(); + ++it + ){ + json_object_object_add(jobj, it->first.c_str(), it->second); + } + return jobj; +} + +std::string json_util::to_message(const std::string& name, JSON_OBJECT_T jobj) { + JSON_OBJECT_T jmsg = json_object_new_array(); + json_object_array_add(jmsg, json_util::from_string(name)); + json_object_array_add(jmsg, jobj); + std::string str (json_object_to_json_string(jmsg)); + return str; +} + +std::string json_util::to_string(JSON_OBJECT_T jobj) { + return std::string(json_object_get_string(jobj)); +} + +template +std::list json_object_to_list(JSON_OBJECT_T jobj, T (*convert)(JSON_OBJECT_T)) { + std::list ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + for (size_t i = 0; i < array_list_length(arr); ++i) { + JSON_OBJECT_T jsub = static_cast (array_list_get_idx(arr, i)); + ret.push_back(convert(jsub)); + } + return ret; +} + +std::pair, int> json_util::to_int_list_int_pair(JSON_OBJECT_T jobj) { + std::pair, int> ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_to_list(first, json_util::to_int); + ret.second = json_object_get_int(second); + + return ret; +} + +std::pair json_util::to_bool_string_pair(JSON_OBJECT_T jobj) { + std::pair ret; + + struct array_list* arr = json_object_get_array(jobj); + if (arr == NULL) + return ret; + + JSON_OBJECT_T first = static_cast (array_list_get_idx(arr, 0)); + JSON_OBJECT_T second = static_cast (array_list_get_idx(arr, 1)); + + ret.first = json_object_get_boolean(first); + ret.second = json_object_get_string(second); + + return ret; +} + +std::list json_util::to_string_list(JSON_OBJECT_T jobj) { + return json_object_to_list(jobj, json_util::to_string); +} + +int json_util::to_int(JSON_OBJECT_T jobj) { + return json_object_get_int(jobj); +} + +bool json_util::to_boolean(JSON_OBJECT_T jobj) { + return json_object_get_boolean(jobj); +} + +void json_util::read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + + // Make some local variables + int BUF_LEN = 24; + char* buf = new char[BUF_LEN]; // buffer for socket read + int buf_len; // length of new bytes in the buffer + int buf_offset; // offset of the JSON parser in the buffer + JSON_OBJECT_T jobj; // pointer to parsed JSON object + json_tokener* tok = json_tokener_new(); // JSON tokenizer instance + enum json_tokener_error jerr; // status of JSON tokenizer + + // Start the blocking I/O loop + while( (buf_len=recv(sockfd, buf, BUF_LEN, 0)) > 0) { + buf_offset = 0; + while(buf_offset < buf_len){ + jobj = json_tokener_parse_ex(tok, buf + buf_offset, buf_len - buf_offset); + jerr = json_tokener_get_error(tok); + buf_offset += tok->char_offset; + + // Do we need more material in order to make JSON? + if (jerr == json_tokener_continue) { + continue; + } + + // Make a new tokenizer + json_tokener_free(tok); + tok = json_tokener_new(); + + // Did we encounter a malformed JSON object? + if (jerr != json_tokener_success) { + fprintf(stderr, + "JSON parse error: %s: '%.*s'\n", + json_tokener_error_desc(jerr), + 1, + buf + buf_offset); + fflush(stderr); + break; + } + + // Our object is ready + process_message(jobj, cb, arg); + } + } + + json_tokener_free(tok); + delete[] buf; +} + +void json_util::process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg) { + if (!json_object_is_type(jobj, json_type_array)) + return; + if (json_object_array_length(jobj) != 2) + return; + + cb( + json_util::to_string(json_object_array_get_idx(jobj, 0)), + json_object_array_get_idx(jobj, 1), + arg + ); +} + +} // namespace octave diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/json-util.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/json-util.h Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,64 @@ +#ifndef json_util_h +#define json_util_h + +#include +#include +#include + +#include "syminfo.h" +#include "event-manager.h" + +class string_vector; + +// All of the code interacting with the external JSON library should be in +// the json-util.h and json-util.cc files. This way, if we want to change +// the external JSON library, we can do it all in one place. + +#define JSON_OBJECT_T json_object* +#define JSON_MAP_T std::map + +#define JSON_MAP_SET(M, FIELD, TYPE){ \ + m[#FIELD] = json_util::from_##TYPE (FIELD); \ +} + +namespace octave { + +class json_util { +public: + static JSON_OBJECT_T from_string(const std::string& str); + static JSON_OBJECT_T from_int(int i); + static JSON_OBJECT_T from_float(float flt); + static JSON_OBJECT_T from_boolean(bool b); + static JSON_OBJECT_T empty(); + + static JSON_OBJECT_T from_string_list(const std::list& list); + static JSON_OBJECT_T from_string_vector(const string_vector& list); + static JSON_OBJECT_T from_int_list(const std::list& list); + static JSON_OBJECT_T from_float_list(const std::list& list); + static JSON_OBJECT_T from_symbol_info_list(const symbol_info_list& list); + static JSON_OBJECT_T from_filter_list(const interpreter_events::filter_list& list); + + static JSON_OBJECT_T from_value_string(const std::string str); + static JSON_OBJECT_T from_symbol_info(const symbol_info element); + static JSON_OBJECT_T from_pair(std::pair pair); + + static JSON_OBJECT_T from_map(JSON_MAP_T m); + + static std::string to_message(const std::string& name, JSON_OBJECT_T jobj); + + static std::string to_string(JSON_OBJECT_T jobj); + static std::pair, int> to_int_list_int_pair(JSON_OBJECT_T jobj); + static std::pair to_bool_string_pair(JSON_OBJECT_T jobj); + static std::list to_string_list(JSON_OBJECT_T jobj); + static int to_int(JSON_OBJECT_T jobj); + static bool to_boolean(JSON_OBJECT_T jobj); + + static void read_stream(int sockfd, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); + +private: + static void process_message(JSON_OBJECT_T jobj, void (*cb)(std::string, JSON_OBJECT_T, void*), void* arg); +}; + +} // namespace octave + +#endif diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/module.mk --- a/libinterp/corefcn/module.mk Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/module.mk Sat Dec 30 13:51:34 2023 -0600 @@ -46,6 +46,8 @@ %reldir%/help.h \ %reldir%/hook-fcn.h \ %reldir%/input.h \ + %reldir%/json-main.h \ + %reldir%/json-util.h \ %reldir%/interpreter.h \ %reldir%/latex-text-renderer.h \ %reldir%/load-path.h \ @@ -77,6 +79,7 @@ %reldir%/oct-strstrm.h \ %reldir%/oct.h \ %reldir%/octave-default-image.h \ + %reldir%/octave-json-link.h \ %reldir%/pager.h \ %reldir%/pr-flt-fmt.h \ %reldir%/pr-output.h \ @@ -163,6 +166,7 @@ %reldir%/error.cc \ %reldir%/errwarn.cc \ %reldir%/event-manager.cc \ + %reldir%/event-manager-url.cc \ %reldir%/event-queue.cc \ %reldir%/fcn-info.cc \ %reldir%/fft.cc \ @@ -189,6 +193,8 @@ %reldir%/hex2num.cc \ %reldir%/hook-fcn.cc \ %reldir%/input.cc \ + %reldir%/json-main.cc \ + %reldir%/json-util.cc \ %reldir%/interpreter-private.cc \ %reldir%/interpreter.cc \ %reldir%/inv.cc \ @@ -228,6 +234,7 @@ %reldir%/oct-tex-lexer.ll \ %reldir%/oct-tex-parser.h \ %reldir%/oct-tex-parser.yy \ + %reldir%/octave-json-link.cc \ %reldir%/ordqz.cc \ %reldir%/ordschur.cc \ %reldir%/pager.cc \ diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/octave-json-link.cc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.cc Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,463 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "octave-json-link.h" +#include "cmd-edit.h" +#include "json-main.h" +#include "json-util.h" + +namespace octave { + +octave_json_link::octave_json_link(json_main* __json_main) + : interpreter_events (), + _json_main (__json_main) +{ + _request_input_enabled = true; + _plot_destination = STATIC_ONLY; +} + +octave_json_link::~octave_json_link(void) { } + +std::string octave_json_link::request_input(const std::string& prompt) { + // Triggered whenever the console prompts for user input + + std::string value; + if (!request_input_queue.dequeue_to(&value)) { + _publish_message("request-input", json_util::from_string(prompt)); + value = request_input_queue.dequeue(); + } + return value; +} + +std::string octave_json_link::request_url(const std::string& url, const std::list& param, const std::string& action, bool& success) { + // Triggered on urlread/urlwrite + + JSON_MAP_T m; + JSON_MAP_SET(m, url, string); + JSON_MAP_SET(m, param, string_list); + JSON_MAP_SET(m, action, string); + + _publish_message("request-url", json_util::from_map(m)); + std::pair result = request_url_queue.dequeue(); + success = result.first; + return result.second; +} + +bool octave_json_link::confirm_shutdown(void) { + // Triggered when the kernel tries to exit + _publish_message("confirm-shutdown", json_util::empty()); + + return confirm_shutdown_queue.dequeue(); +} + +// do_exit was removed in Octave 5 +// bool octave_json_link::do_exit(int status) { +// JSON_MAP_T m; +// JSON_MAP_SET(m, status, int); +// _publish_message("exit", json_util::from_map(m)); + +// // It is our responsibility in octave_link to call exit. If we don't, then +// // the kernel waits for 24 hours expecting us to do something. +// ::exit(status); + +// return true; +// } + +bool octave_json_link::copy_image_to_clipboard(const std::string& file) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("copy-image-to-clipboard", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::edit_file(const std::string& file) { + // Triggered in "edit" for existing files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("edit-file", json_util::from_map(m)); + + return true; +} + +bool octave_json_link::prompt_new_edit_file(const std::string& file) { + // Triggered in "edit" for new files + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + _publish_message("prompt-new-edit-file", json_util::from_map(m)); + + return prompt_new_edit_file_queue.dequeue(); +} + +// int octave_json_link::do_message_dialog(const std::string& dlg, const std::string& msg, const std::string& title) { +// // Triggered in "msgbox", "helpdlg", and "errordlg", among others +// JSON_MAP_T m; +// JSON_MAP_SET(m, dlg, string); // i.e., m["dlg"] = json_util::from_string(dlg); +// JSON_MAP_SET(m, msg, string); +// JSON_MAP_SET(m, title, string); +// _publish_message("message-dialog", json_util::from_map(m)); + +// return message_dialog_queue.dequeue(); +// } + +bool octave_json_link::have_dialogs() const { + // Triggered in "inputdlg" and similar functions to check for dialog support + return true; +} + +std::string octave_json_link::question_dialog(const std::string& msg, const std::string& title, const std::string& btn1, const std::string& btn2, const std::string& btn3, const std::string& btndef) { + // Triggered in "questdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, msg, string); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, btn1, string); + JSON_MAP_SET(m, btn2, string); + JSON_MAP_SET(m, btn3, string); + JSON_MAP_SET(m, btndef, string); + _publish_message("question-dialog", json_util::from_map(m)); + + return question_dialog_queue.dequeue(); +} + +std::pair, int> octave_json_link::list_dialog(const std::list& list, const std::string& mode, int width, int height, const std::list& initial_value, const std::string& name, const std::list& prompt, const std::string& ok_string, const std::string& cancel_string) { + // Triggered in "listdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, list, string_list); + JSON_MAP_SET(m, mode, string); + JSON_MAP_SET(m, width, int); + JSON_MAP_SET(m, height, int); + JSON_MAP_SET(m, initial_value, int_list); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, ok_string, string); + JSON_MAP_SET(m, cancel_string, string); + _publish_message("list-dialog", json_util::from_map(m)); + + return list_dialog_queue.dequeue(); +} + +std::list octave_json_link::input_dialog(const std::list& prompt, const std::string& title, const std::list& nr, const std::list& nc, const std::list& defaults) { + // Triggered in "inputdlg" + JSON_MAP_T m; + JSON_MAP_SET(m, prompt, string_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, nr, float_list); + JSON_MAP_SET(m, nc, float_list); + JSON_MAP_SET(m, defaults, string_list); + _publish_message("input-dialog", json_util::from_map(m)); + + return input_dialog_queue.dequeue(); +} + +std::list octave_json_link::file_dialog(const filter_list& filter, const std::string& title, const std::string &filename, const std::string &pathname, const std::string& multimode) { + // Triggered in "uiputfile", "uigetfile", and "uigetdir" + JSON_MAP_T m; + JSON_MAP_SET(m, filter, filter_list); + JSON_MAP_SET(m, title, string); + JSON_MAP_SET(m, filename, string); + JSON_MAP_SET(m, pathname, string); + JSON_MAP_SET(m, multimode, string); + _publish_message("file-dialog", json_util::from_map(m)); + + return file_dialog_queue.dequeue(); +} + +void octave_json_link::update_path_dialog(void) { + // Triggered in "rehash" + _publish_message("update-path-dialog", json_util::empty()); +} + +int octave_json_link::debug_cd_or_addpath_error(const std::string& file, const std::string& dir, bool addpath_option) { + // This endpoint might be unused? (No references) + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, dir, string); + JSON_MAP_SET(m, addpath_option, boolean); + _publish_message("debug-cd-or-addpath-error", json_util::from_map(m)); + + return debug_cd_or_addpath_error_queue.dequeue(); +} + +void octave_json_link::focus_window(const std::string win_name) { + // Triggered in "commandhistory", "commandwindow", "filebrowser", "workspace" + JSON_MAP_T m; + JSON_MAP_SET(m, win_name, string); + _publish_message("focus-window", json_util::from_map(m)); +} + +void octave_json_link::display_exception(const execution_exception& ee, bool beep) { + // Triggered in various places in libinterp + std::ostringstream buf; + ee.display (buf); + std::string ee_str = buf.str(); + JSON_MAP_T m; + JSON_MAP_SET(m, ee_str, string); + JSON_MAP_SET(m, beep, boolean); + _publish_message("display-exception", json_util::from_map(m)); +} + +void octave_json_link::display_warning(const std::string& id, const std::string& name, const std::string& message, const std::string& formatted) { + // Redirected warnings from std::cerr + JSON_MAP_T m; + JSON_MAP_SET(m, id, string); + JSON_MAP_SET(m, name, string); + JSON_MAP_SET(m, message, string); + JSON_MAP_SET(m, formatted, string); + _publish_message("display-warning", json_util::from_map(m)); +} + +void octave_json_link::gui_status_update(const std::string& feature, const std::string& status) { + // Triggered in __profiler_enable__ + JSON_MAP_T m; + JSON_MAP_SET(m, feature, string); + JSON_MAP_SET(m, status, string); + _publish_message("gui-status-update", json_util::from_map(m)); +} + +void octave_json_link::update_gui_lexer(void) { + // Triggered in "load_packages_and_dependencies" + _publish_message("update-gui-lexer", json_util::empty()); +} + +void octave_json_link::directory_changed(const std::string& dir) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, dir, string); + _publish_message("change-directory", json_util::from_map(m)); +} + +void octave_json_link::file_remove (const std::string& old_name, const std::string& new_name) { + // Called by "unlink", "rmdir", "rename" + JSON_MAP_T m; + JSON_MAP_SET(m, old_name, string); + JSON_MAP_SET(m, new_name, string); + _publish_message("file-remove", json_util::from_map(m)); +} + +void octave_json_link::file_renamed (bool status) { + // Called by "unlink", "rmdir", "rename" + _publish_message("file-renamed", json_util::from_boolean(status)); +} + +void octave_json_link::execute_command_in_terminal(const std::string& command) { + // This endpoint might be unused? (References appear only in libgui) + JSON_MAP_T m; + JSON_MAP_SET(m, command, string); + _publish_message("execute-command-in-terminal", json_util::from_map(m)); +} + +uint8NDArray octave_json_link::get_named_icon (const std::string& /* icon_name */) { + // Called from msgbox.m + // TODO: Implement request/response for this event + uint8NDArray retval; + return retval; +} + +void octave_json_link::set_workspace(bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) { + // Triggered on every new line entry + JSON_MAP_T m; + JSON_MAP_SET(m, top_level, boolean); + JSON_MAP_SET(m, debug, boolean); + JSON_MAP_SET(m, ws, symbol_info_list); + JSON_MAP_SET(m, update_variable_editor, boolean); + _publish_message("set-workspace", json_util::from_map(m)); +} + +void octave_json_link::clear_workspace(void) { + // Triggered on "clear" command (but not "clear all" or "clear foo") + _publish_message("clear-workspace", json_util::empty()); +} + +void octave_json_link::set_history(const string_vector& hist) { + // Called at startup, possibly more? + JSON_MAP_T m; + JSON_MAP_SET(m, hist, string_vector); + _publish_message("set-history", json_util::from_map(m)); +} + +void octave_json_link::append_history(const std::string& hist_entry) { + // Appears to be tied to readline, if available + JSON_MAP_T m; + JSON_MAP_SET(m, hist_entry, string); + _publish_message("append-history", json_util::from_map(m)); +} + +void octave_json_link::clear_history(void) { + // Appears to be tied to readline, if available + _publish_message("clear-history", json_util::empty()); +} + +void octave_json_link::clear_screen(void) { + // Triggered by clc + _publish_message("clear-screen", json_util::empty()); +} + +void octave_json_link::pre_input_event(void) { + // noop +} + +void octave_json_link::post_input_event(void) { + // noop +} + +void octave_json_link::enter_debugger_event(const std::string& fcn_name, const std::string& fcn_file_name, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, fcn_name, string); + JSON_MAP_SET(m, fcn_file_name, string); + JSON_MAP_SET(m, line, int); + _publish_message("enter-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::execute_in_debugger_event(const std::string& file, int line) { + JSON_MAP_T m; + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + _publish_message("execute-in-debugger-event", json_util::from_map(m)); +} + +void octave_json_link::exit_debugger_event(void) { + _publish_message("exit-debugger-event", json_util::empty()); +} + +void octave_json_link::update_breakpoint(bool insert, const std::string& file, int line, const std::string& cond) { + JSON_MAP_T m; + JSON_MAP_SET(m, insert, boolean); + JSON_MAP_SET(m, file, string); + JSON_MAP_SET(m, line, int); + JSON_MAP_SET(m, cond, string); + _publish_message("update-breakpoint", json_util::from_map(m)); +} + +// void octave_json_link::do_set_default_prompts(std::string& ps1, std::string& ps2, std::string& ps4) { +// // Triggered upon interpreter startup +// JSON_MAP_T m; +// JSON_MAP_SET(m, ps1, string); +// JSON_MAP_SET(m, ps2, string); +// JSON_MAP_SET(m, ps4, string); +// _publish_message("set-default-prompts", json_util::from_map(m)); +// } + +void octave_json_link::show_preferences(void) { + // Triggered on "preferences" command + _publish_message("show-preferences", json_util::empty()); +} + +std::string octave_json_link::gui_preference (const std::string& /* key */, const std::string& /* value */) { + // Used by Octave GUI? + // TODO: Implement request/response for this event + std::string retval; + return retval; +} + +bool octave_json_link::show_documentation(const std::string& file) { + // Triggered on "doc" command + _publish_message("show-doc", json_util::from_string(file)); + return true; +} + +void octave_json_link::register_documentation (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("register-doc", json_util::from_string(file)); +} + +void octave_json_link::unregister_documentation (const std::string& file) { + // Triggered by the GUI documentation viewer? + _publish_message("unregister-doc", json_util::from_string(file)); +} + +void octave_json_link::edit_variable (const std::string& name, const octave_value& /* val */) { + // Triggered on "openvar" command + JSON_MAP_T m; + JSON_MAP_SET(m, name, string); + // TODO: val + _publish_message("edit-variable", json_util::from_map(m)); +} + +void octave_json_link::show_static_plot(const std::string& term, const std::string& content) { + // Triggered on all plot commands with setenv("GNUTERM","svg") + int command_number = command_editor::current_command_number(); + JSON_MAP_T m; + JSON_MAP_SET(m, term, string); + JSON_MAP_SET(m, content, string); + JSON_MAP_SET(m, command_number, int); + _publish_message("show-static-plot", json_util::from_map(m)); +} + +void octave_json_link::receive_message (const std::string& name, JSON_OBJECT_T jobj) { + if (name == "cmd" || name == "request-input-answer") { + std::string answer = json_util::to_string(jobj); + request_input_queue.enqueue(answer); + } + else if (name == "request-url-answer") { + std::pair answer = json_util::to_bool_string_pair(jobj); + request_url_queue.enqueue(answer); + } + else if (name == "confirm-shutdown-answer"){ + bool answer = json_util::to_boolean(jobj); + confirm_shutdown_queue.enqueue(answer); + } + else if (name == "prompt-new-edit-file-answer"){ + bool answer = json_util::to_boolean(jobj); + prompt_new_edit_file_queue.enqueue(answer); + } + else if (name == "message-dialog-answer"){ + int answer = json_util::to_int(jobj); + message_dialog_queue.enqueue(answer); + } + else if (name == "question-dialog-answer") { + std::string answer = json_util::to_string(jobj); + question_dialog_queue.enqueue(answer); + } + else if (name == "list-dialog-answer") { + std::pair, int> answer = json_util::to_int_list_int_pair(jobj); + list_dialog_queue.enqueue(answer); + } + else if (name == "input-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + input_dialog_queue.enqueue(answer); + } + else if (name == "file-dialog-answer") { + std::list answer = json_util::to_string_list(jobj); + file_dialog_queue.enqueue(answer); + } + else if (name == "debug-cd-or-addpath-error-answer") { + int answer = json_util::to_int(jobj); + debug_cd_or_addpath_error_queue.enqueue(answer); + } + else { + std::cerr << "warning: received unknown message: " << name << std::endl; + } +} + +void octave_json_link::_publish_message(const std::string& name, JSON_OBJECT_T jobj) { + _json_main->publish_message(name, jobj); +} + +} // namespace octave diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/octave-json-link.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libinterp/corefcn/octave-json-link.h Sat Dec 30 13:51:34 2023 -0600 @@ -0,0 +1,269 @@ +/* + +Copyright (C) 2015-2016 Shane Carr + +This file is part of Octave. + +Octave is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3 of the License, or (at your +option) any later version. + +Octave is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with Octave; see the file COPYING. If not, see +. + +*/ + +#ifndef octave_json_link_h +#define octave_json_link_h + +#include +#include + +#include "event-manager.h" +#include "json-util.h" +#include "oct-mutex.h" + +class string_vector; + +namespace octave { + +// Circular reference +class json_main; + +// Thread-safe queue +template class json_queue { +public: + json_queue(); + ~json_queue(); + + void enqueue(const T& value); + T dequeue(); + bool dequeue_to(T* destination); + +private: + std::queue _queue; + mutex _mutex; +}; + +class octave_json_link : public interpreter_events +{ + +public: + + octave_json_link (json_main* __json_main); + + ~octave_json_link (void); + + // TODO + // void start_gui (bool gui_app = false) override; + // void close_gui (void) override; + + bool have_dialogs (void) const override; + + std::string + question_dialog (const std::string& msg, const std::string& title, + const std::string& btn1, const std::string& btn2, + const std::string& btn3, const std::string& btndef) override; + + std::pair, int> + list_dialog (const std::list& list, + const std::string& mode, + int width, int height, + const std::list& initial_value, + const std::string& name, + const std::list& prompt, + const std::string& ok_string, + const std::string& cancel_string) override; + + std::list + input_dialog (const std::list& prompt, + const std::string& title, + const std::list& nr, + const std::list& nc, + const std::list& defaults) override; + + std::list + file_dialog (const filter_list& filter, const std::string& title, + const std::string &filename, const std::string &pathname, + const std::string& multimode) override; + + void update_path_dialog (void) override; + + void show_preferences (void) override; + + // TODO: + // void apply_preferences (void) override; + + // TODO: + // void show_terminal_window (void) override; + + bool show_documentation (const std::string& file) override; + + // TODO: + // void show_file_browser (void) override; + + // TODO: + // void show_command_history (void) override; + + // TODO: + // void show_workspace (void) override; + + // TODO: + // void show_community_news (int serial) override; + // void show_release_notes (void) override; + + bool edit_file (const std::string& file) override; + + void edit_variable (const std::string& name, const octave_value& val) override; + + std::string request_input (const std::string& prompt) override; + + std::string request_url (const std::string& url, const std::list& param, const std::string& action, bool& success) override; + + void show_static_plot (const std::string& term, const std::string& content) override; + + bool confirm_shutdown (void) override; + + bool prompt_new_edit_file (const std::string& file) override; + + int + debug_cd_or_addpath_error (const std::string& file, + const std::string& dir, + bool addpath_option) override; + + uint8NDArray get_named_icon (const std::string& icon_name) override; + + std::string gui_preference (const std::string& key, const std::string& value) override; + + bool copy_image_to_clipboard (const std::string& file) override; + + void focus_window (const std::string win_name) override; + + void execute_command_in_terminal (const std::string& command) override; + + void register_documentation (const std::string& file) override; + + void unregister_documentation (const std::string& file) override; + + // TODO: + // void interpreter_output (const std::string& msg) override; + + void display_exception (const execution_exception& ee, bool beep) override; + + void display_warning (const std::string& id, const std::string& name, const std::string& message, const std::string& formatted) override; + + void gui_status_update (const std::string& feature, const std::string& status) override; + + void update_gui_lexer (void) override; + + void directory_changed (const std::string& dir) override; + + void file_remove (const std::string& old_name, const std::string& new_name) override; + + void file_renamed (bool) override; + + void set_workspace (bool top_level, bool debug, + const symbol_info_list& ws, + bool update_variable_editor) override; + + void clear_workspace (void) override; + + // TODO: + // void update_prompt (const std::string& prompt) override; + + void set_history (const string_vector& hist) override; + + void append_history (const std::string& hist_entry) override; + + void clear_history (void) override; + + void clear_screen (void) override; + + void pre_input_event (void) override; + + void post_input_event (void) override; + + void enter_debugger_event (const std::string& fcn_name, const std::string& fcn_file_name, int line) override; + + void execute_in_debugger_event (const std::string& file, int line) override; + + void exit_debugger_event (void) override; + + void update_breakpoint (bool insert, + const std::string& file, int line, + const std::string& cond) override; + + // TODO: + // void interpreter_interrupted (void) override; + + // Custom methods + void receive_message (const std::string& name, JSON_OBJECT_T jobj); + +private: + json_main* _json_main; + void _publish_message (const std::string& name, JSON_OBJECT_T jobj); + + // Queues + json_queue request_input_queue; + json_queue > request_url_queue; + json_queue confirm_shutdown_queue; + json_queue prompt_new_edit_file_queue; + json_queue message_dialog_queue; + json_queue question_dialog_queue; + json_queue, int> > list_dialog_queue; + json_queue > input_dialog_queue; + json_queue > file_dialog_queue; + json_queue debug_cd_or_addpath_error_queue; +}; + +// Template classes require definitions in the header file... + +template +json_queue::json_queue() { } + +template +json_queue::~json_queue() { } + +template +void json_queue::enqueue(const T& value) { + _mutex.lock(); + _queue.push(value); + _mutex.cond_signal(); + _mutex.unlock(); +} + +template +T json_queue::dequeue() { + _mutex.lock(); + while (_queue.empty()) { + _mutex.cond_wait(); + } + T value = _queue.front(); + _queue.pop(); + _mutex.unlock(); + return value; +} + +template +bool json_queue::dequeue_to(T* destination) { + _mutex.lock(); + bool retval = false; + if (!_queue.empty()) { + retval = true; + *destination = _queue.front(); + _queue.pop(); + } + _mutex.unlock(); + return retval; +} + +} // namespace octave + +#endif diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/syscalls.cc --- a/libinterp/corefcn/syscalls.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/syscalls.cc Sat Dec 30 13:51:34 2023 -0600 @@ -150,9 +150,8 @@ @deftypefn {} {[@var{err}, @var{msg}] =} exec (@var{file}, @var{args}) Replace current process with a new process. -Calling @code{exec} without first calling @code{fork} will terminate your -current Octave process and replace it with the program named by @var{file}. -For example, +Calling @code{exec} will terminate your current Octave process and replace +it with the program named by @var{file}. For example, @example exec ("ls", "-l") @@ -475,42 +474,6 @@ return retval; } -DEFMETHODX ("fork", Ffork, interp, args, , - doc: /* -*- texinfo -*- -@deftypefn {} {[@var{pid}, @var{msg}] =} fork () -Create a copy of the current process. - -Fork can return one of the following values: - -@table @asis -@item > 0 -You are in the parent process. The value returned from @code{fork} is the -process id of the child process. You should probably arrange to wait for -any child processes to exit. - -@item 0 -You are in the child process. You can call @code{exec} to start another -process. If that fails, you should probably call @code{exit}. - -@item < 0 -The call to @code{fork} failed for some reason. You must take evasive -action. A system dependent error message will be waiting in @var{msg}. -@end table -@end deftypefn */) -{ - if (args.length () != 0) - print_usage (); - - if (interp.at_top_level ()) - error ("fork: cannot be called from command line"); - - std::string msg; - - pid_t pid = sys::fork (msg); - - return ovl (pid, msg); -} - DEFUNX ("getpgrp", Fgetpgrp, args, , doc: /* -*- texinfo -*- @deftypefn {} {pgid =} getpgrp () diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/sysdep.cc --- a/libinterp/corefcn/sysdep.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/sysdep.cc Sat Dec 30 13:51:34 2023 -0600 @@ -75,6 +75,7 @@ #include "defun.h" #include "error.h" #include "errwarn.h" +#include "event-manager.h" #include "input.h" #include "interpreter-private.h" #include "octave.h" @@ -719,7 +720,7 @@ // Read one character from the terminal. -int kbhit (bool wait) +int kbhit (const std::string& prompt, bool wait) { #if defined (HAVE__KBHIT) && defined (HAVE__GETCH) // This essentially means we are on a Windows system. @@ -746,13 +747,24 @@ set_interrupt_handler (saved_interrupt_handler, false); - int c = std::cin.get (); + int c; + event_manager& evmgr = __get_event_manager__ (); + if (evmgr.request_input_enabled ()) { + std::string line = evmgr.request_input (prompt); + if (line.length() >= 1) { + c = line.at(0); + } else { + c = '\n'; + } + } else { + c = std::cin.get (); - if (std::cin.fail () || std::cin.eof ()) - { - std::cin.clear (); - clearerr (stdin); - } + if (std::cin.fail () || std::cin.eof ()) + { + std::cin.clear (); + clearerr (stdin); + } + } // Restore it, enabling system call restarts (if possible). set_interrupt_handler (saved_interrupt_handler, true); @@ -810,6 +822,9 @@ { bool skip_redisplay = true; + octave::event_manager& evmgr = octave::__get_event_manager__ (); + evmgr.clear_screen(); + command_editor::clear_screen (skip_redisplay); return ovl (); @@ -1244,7 +1259,7 @@ Fdrawnow (interp); - int c = kbhit (args.length () == 0); + int c = kbhit ("kbhit>", args.length () == 0); if (c == -1) c = 0; diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/sysdep.h --- a/libinterp/corefcn/sysdep.h Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/sysdep.h Sat Dec 30 13:51:34 2023 -0600 @@ -49,7 +49,7 @@ extern OCTINTERP_API int pclose (FILE *f); -extern OCTINTERP_API int kbhit (bool wait = true); +extern OCTINTERP_API int kbhit (const std::string& prompt, bool wait); extern OCTINTERP_API std::string get_P_tmpdir (void); diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/corefcn/utils.cc --- a/libinterp/corefcn/utils.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/corefcn/utils.cc Sat Dec 30 13:51:34 2023 -0600 @@ -1556,7 +1556,7 @@ if (do_graphics_events) gh_mgr.process_events (); - c = kbhit (false); + c = kbhit ("press enter to continue", false); } } else diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/dldfcn/__init_gnuplot__.cc --- a/libinterp/dldfcn/__init_gnuplot__.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/dldfcn/__init_gnuplot__.cc Sat Dec 30 13:51:34 2023 -0600 @@ -65,25 +65,6 @@ gnuplot_graphics_toolkit (octave::interpreter& interp) : octave::base_graphics_toolkit ("gnuplot"), m_interpreter (interp) { - static bool warned = false; - - if (! warned) - { - warning_with_id - ("Octave:gnuplot-graphics", - "using the gnuplot graphics toolkit is discouraged\n\ -\n\ -The gnuplot graphics toolkit is not actively maintained and has a number\n\ -of limitations that are unlikely to be fixed. Communication with gnuplot\n\ -uses a one-directional pipe and limited information is passed back to the\n\ -Octave interpreter so most changes made interactively in the plot window\n\ -will not be reflected in the graphics properties managed by Octave. For\n\ -example, if the plot window is closed with a mouse click, Octave will not\n\ -be notified and will not update its internal list of open figure windows.\n\ -The qt toolkit is recommended instead.\n"); - - warned = true; - } } ~gnuplot_graphics_toolkit (void) = default; diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/octave.cc --- a/libinterp/octave.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/octave.cc Sat Dec 30 13:51:34 2023 -0600 @@ -186,6 +186,16 @@ case LINE_EDITING_OPTION: m_forced_line_editing = m_line_editing = true; break; + + case JSON_SOCK_OPTION: + if (octave_optarg_wrapper ()) + m_json_sock_path = octave_optarg_wrapper (); + break; + + case JSON_MAX_LEN_OPTION: + if (octave_optarg_wrapper ()) + m_json_max_message_length = strtol(octave_optarg_wrapper (), NULL, 10); + break; case NO_GUI_OPTION: m_gui = false; @@ -420,6 +430,14 @@ sysdep_init (); } +bool application::link_enabled (void) const +{ + if (m_interpreter) { + event_manager& evmgr = m_interpreter->get_event_manager (); + return evmgr.enabled(); + } else return false; +} + int cli_application::execute (void) { interpreter& interp = create_interpreter (); @@ -442,7 +460,7 @@ // FIXME: This isn't quite right, it just says that we intended to // start the GUI, not that it is actually running. - return ovl (application::is_gui_running ()); + return ovl (application::is_link_enabled ()); } /* diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/octave.h --- a/libinterp/octave.h Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/octave.h Sat Dec 30 13:51:34 2023 -0600 @@ -85,6 +85,8 @@ std::string info_file (void) const { return m_info_file; } std::string info_program (void) const { return m_info_program; } std::string texi_macros_file (void) const {return m_texi_macros_file; } + std::string json_sock_path (void) const { return m_json_sock_path; } + int json_max_message_length (void) const { return m_json_max_message_length; } string_vector all_args (void) const { return m_all_args; } string_vector remaining_args (void) const { return m_remaining_args; } @@ -117,6 +119,8 @@ void info_file (const std::string& arg) { m_info_file = arg; } void info_program (const std::string& arg) { m_info_program = arg; } void texi_macros_file (const std::string& arg) { m_texi_macros_file = arg; } + void json_sock_path (const std::string& arg) { m_json_sock_path = arg; } + void json_max_message_length (int arg) { m_json_max_message_length = arg; } void all_args (const string_vector& arg) { m_all_args = arg; } void remaining_args (const string_vector& arg) { m_remaining_args = arg; } @@ -225,6 +229,14 @@ // (--texi-macros-file) std::string m_texi_macros_file; + // The value for "JSON_SOCK" specified on the command line. + // (--json-sock) + std::string m_json_sock_path; + + // The maximum message length; valid only if "JSON_SOCK" is specified. + // (--json-max-len) + int m_json_max_message_length = 0; + // All arguments passed to the argc, argv constructor. string_vector m_all_args; @@ -238,6 +250,7 @@ // both) of them... class interpreter; +class event_manager; // Base class for an Octave application. @@ -287,6 +300,8 @@ virtual bool gui_running (void) const { return false; } virtual void gui_running (bool) { } + bool link_enabled (void) const; + void program_invocation_name (const std::string& nm) { m_program_invocation_name = nm; } @@ -320,6 +335,11 @@ return s_instance ? s_instance->gui_running () : false; } + static bool is_link_enabled (void) + { + return s_instance ? s_instance->link_enabled () : false; + } + // Convenience functions. static bool forced_interactive (void); diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/options.h --- a/libinterp/options.h Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/options.h Sat Dec 30 13:51:34 2023 -0600 @@ -46,17 +46,19 @@ #define IMAGE_PATH_OPTION 7 #define INFO_FILE_OPTION 8 #define INFO_PROG_OPTION 9 -#define LINE_EDITING_OPTION 10 -#define NO_GUI_OPTION 11 -#define NO_GUI_LIBS_OPTION 12 -#define NO_INIT_FILE_OPTION 13 -#define NO_INIT_PATH_OPTION 14 -#define NO_LINE_EDITING_OPTION 15 -#define NO_SITE_FILE_OPTION 16 -#define PERSIST_OPTION 17 -#define SERVER_OPTION 18 -#define TEXI_MACROS_FILE_OPTION 19 -#define TRADITIONAL_OPTION 20 +#define JSON_SOCK_OPTION 10 +#define JSON_MAX_LEN_OPTION 11 +#define LINE_EDITING_OPTION 12 +#define NO_GUI_OPTION 13 +#define NO_GUI_LIBS_OPTION 14 +#define NO_INIT_FILE_OPTION 15 +#define NO_INIT_PATH_OPTION 16 +#define NO_LINE_EDITING_OPTION 17 +#define NO_SITE_FILE_OPTION 18 +#define PERSIST_OPTION 19 +#define SERVER_OPTION 20 +#define TEXI_MACROS_FILE_OPTION 21 +#define TRADITIONAL_OPTION 22 struct octave_getopt_options long_opts[] = { { "braindead", octave_no_arg, nullptr, TRADITIONAL_OPTION }, @@ -74,6 +76,8 @@ { "info-file", octave_required_arg, nullptr, INFO_FILE_OPTION }, { "info-program", octave_required_arg, nullptr, INFO_PROG_OPTION }, { "interactive", octave_no_arg, nullptr, 'i' }, + { "json-sock", octave_required_arg, nullptr, JSON_SOCK_OPTION }, + { "json-max-len", octave_required_arg, nullptr, JSON_MAX_LEN_OPTION }, { "line-editing", octave_no_arg, nullptr, LINE_EDITING_OPTION }, { "no-gui", octave_no_arg, nullptr, NO_GUI_OPTION }, { "no-gui-libs", octave_no_arg, nullptr, NO_GUI_LIBS_OPTION }, diff -r 78c13a2594f3 -r d2250ae9bddd libinterp/usage.h --- a/libinterp/usage.h Sun Nov 05 12:45:40 2023 -0500 +++ b/libinterp/usage.h Sat Dec 30 13:51:34 2023 -0600 @@ -37,11 +37,11 @@ "octave [-HVWdfhiqvx] [--debug] [--doc-cache-file file] [--echo-commands]\n\ [--eval CODE] [--exec-path path] [--experimental-terminal-widget]\n\ [--gui] [--help] [--image-path path] [--info-file file]\n\ - [--info-program prog] [--interactive] [--line-editing] [--no-gui]\n\ - [--no-history] [--no-init-file] [--no-init-path] [--no-line-editing]\n\ - [--no-site-file] [--no-window-system] [--norc] [-p path]\n\ - [--path path] [--persist] [--server] [--silent] [--traditional]\n\ - [--verbose] [--version] [file]"; + [--info-program prog] [--interactive] [--json-sock] [--json-max-len] \n\ + [--line-editing] [--no-gui] [--no-history] [--no-init-file] \n\ + [--no-init-path] [--no-line-editing] [--no-site-file] \n\ + [--no-window-system] [--norc] [-p path] [--path path] [--persist] \n\ + [--server] [--silent] [--traditional] [--verbose] [--version] [file]"; // Usage message with extra help. @@ -69,6 +69,8 @@ --info-file FILE Use top-level info file FILE.\n\ --info-program PROGRAM Use PROGRAM for reading info files.\n\ --interactive, -i Force interactive behavior.\n\ + --json-sock PATH Listen to and publish events on this UNIX socket.\n\ + --json-max-len LEN Suppress JSON messages greater than LEN bytes.\n\ --line-editing Force readline use for command-line editing.\n\ --no-gui Disable the graphical user interface.\n\ --no-history, -H Don't save commands to the history list\n\ diff -r 78c13a2594f3 -r d2250ae9bddd liboctave/util/oct-mutex.cc --- a/liboctave/util/oct-mutex.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/liboctave/util/oct-mutex.cc Sat Dec 30 13:51:34 2023 -0600 @@ -58,6 +58,18 @@ return false; } + void + base_mutex::cond_wait (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + + void + base_mutex::cond_signal (void) + { + (*current_liboctave_error_handler) ("mutex not supported on this platform"); + } + #if defined (OCTAVE_USE_WINDOWS_API) class @@ -68,11 +80,13 @@ : base_mutex () { InitializeCriticalSection (&cs); + InitializeConditionVariable (&cv); } ~w32_mutex (void) { DeleteCriticalSection (&cs); + // no need to delete cv: http://stackoverflow.com/a/28981408/1407170 } void lock (void) @@ -90,8 +104,19 @@ return (TryEnterCriticalSection (&cs) != 0); } + void cond_wait (void) + { + SleepConditionVariableCS (&cv, &cs, INFINITE); + } + + void cond_signal (void) + { + WakeConditionVariable (&cv); + } + private: CRITICAL_SECTION cs; + CONDITION_VARIABLE cv; }; static DWORD thread_id = 0; @@ -123,11 +148,18 @@ pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&m_pm, &attr); pthread_mutexattr_destroy (&attr); + + pthread_condattr_t condattr; + + pthread_condattr_init (&condattr); + pthread_cond_init (&condv, &condattr); + pthread_condattr_destroy (&condattr); } ~pthread_mutex (void) { pthread_mutex_destroy (&m_pm); + pthread_cond_destroy (&condv); } void lock (void) @@ -145,8 +177,19 @@ return (pthread_mutex_trylock (&m_pm) == 0); } + void cond_wait (void) + { + pthread_cond_wait (&condv, &m_pm); + } + + void cond_signal (void) + { + pthread_cond_signal (&condv); + } + private: pthread_mutex_t m_pm; + pthread_cond_t condv; }; static pthread_t thread_id = 0; diff -r 78c13a2594f3 -r d2250ae9bddd liboctave/util/oct-mutex.h --- a/liboctave/util/oct-mutex.h Sun Nov 05 12:45:40 2023 -0500 +++ b/liboctave/util/oct-mutex.h Sat Dec 30 13:51:34 2023 -0600 @@ -50,6 +50,10 @@ virtual void unlock (void); virtual bool try_lock (void); + + virtual void cond_wait (void); + + virtual void cond_signal (void); }; class @@ -80,6 +84,16 @@ return m_rep->try_lock (); } + void cond_wait (void) + { + m_rep->cond_wait (); + } + + void cond_signal (void) + { + m_rep->cond_signal (); + } + protected: std::shared_ptr m_rep; }; diff -r 78c13a2594f3 -r d2250ae9bddd liboctave/util/url-transfer.cc --- a/liboctave/util/url-transfer.cc Sun Nov 05 12:45:40 2023 -0500 +++ b/liboctave/util/url-transfer.cc Sat Dec 30 13:51:34 2023 -0600 @@ -31,6 +31,7 @@ #include #include +#include "base64-wrappers.h" #include "dir-ops.h" #include "file-ops.h" #include "file-stat.h" @@ -228,6 +229,8 @@ return file_list; } + + #if defined (HAVE_CURL) static int @@ -924,18 +927,6 @@ # define REP_CLASS base_url_transfer #endif -url_transfer::url_transfer (void) : m_rep (new REP_CLASS ()) -{ } - -url_transfer::url_transfer (const std::string& host, const std::string& user, - const std::string& passwd, std::ostream& os) - : m_rep (new REP_CLASS (host, user, passwd, os)) -{ } - -url_transfer::url_transfer (const std::string& url, std::ostream& os) - : m_rep (new REP_CLASS (url, os)) -{ } - #undef REP_CLASS OCTAVE_END_NAMESPACE(octave) diff -r 78c13a2594f3 -r d2250ae9bddd scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Sun Nov 05 12:45:40 2023 -0500 +++ b/scripts/help/__unimplemented__.m Sat Dec 30 13:51:34 2023 -0600 @@ -45,7 +45,30 @@ is_matlab_function = true; + ## First look at the package metadata + # To generate package_metadata.mat, run: packages={}; for p=pkg('list'); packages={packages{:} pkg('describe', '-verbose', p{1}.name){:}}; endfor; save('/usr/local/share/octave/site/m/package_metadata.mat', 'packages'); + found_in_package_metadata = false; + try + vars = load("/usr/local/share/octave/site/m/package_metadata.mat"); + for lvl1 = vars.packages + for lvl2 = lvl1{1}.provides + for lvl3 = lvl2{1}.functions + if strcmp(fcn, lvl3{1}) + txt = check_package(fcn, lvl1{1}.name); + found_in_package_metadata = true; + break; + endif + endfor + if found_in_package_metadata, break; endif + endfor + if found_in_package_metadata, break; endif + endfor + catch err + warning(err) + end_try_catch + ## Some smarter cases, add more as needed. + if !found_in_package_metadata switch (fcn) case {"avifile", "aviinfo", "aviread"} txt = ["Basic video file support is provided in the video package. ", ... @@ -524,6 +547,7 @@ txt = ""; endif endswitch + endif if (is_matlab_function) txt = [txt, "\n\n@noindent\nPlease read ", ... @@ -566,13 +590,13 @@ endfor txt = sprintf ("%s but has not yet been implemented.", txt); case "not loaded", - txt = sprintf (["%s which you have installed but not loaded. To ", ... - "load the package, run 'pkg load %s' from the ", ... - "Octave prompt."], txt, name); + txt = sprintf (["%s, which you have installed but not loaded.\n\n", ... + "Run `pkg load %s' to use `%s'."], ... + txt, name, fcn); otherwise ## this includes "not installed" and anything else if pkg changes ## the output of describe - txt = sprintf ("%s which seems to not be installed in your system.", txt); + txt = sprintf ("%s, which seems to not be installed in your system.", txt); endswitch endfunction diff -r 78c13a2594f3 -r d2250ae9bddd scripts/plot/util/__gnuplot_drawnow__.m --- a/scripts/plot/util/__gnuplot_drawnow__.m Sun Nov 05 12:45:40 2023 -0500 +++ b/scripts/plot/util/__gnuplot_drawnow__.m Sat Dec 30 13:51:34 2023 -0600 @@ -32,9 +32,84 @@ if (nargin < 1 || nargin == 2) print_usage (); - endif + + elseif (nargin >= 3 && nargin <= 4) + ## Write the plot to the given file (e.g., via the "print" command) + if (nargin == 5) + __gnuplot_draw_to_file__ (h, term, file, debug_file); + else + __gnuplot_draw_to_file__ (h, term, file); + endif + + else # nargin == 1 + ## Plot to terminal and/or static (e.g., via the "plot" command) + plot_stream = get (h, "__plot_stream__"); + if (isempty (plot_stream)) + plot_stream = __gnuplot_open_stream__ (2, h); + new_stream = true; + else + new_stream = false; + endif + term = gnuplot_default_term (plot_stream); + + ## There are a few options for how we can proceed. + ## In most cases, we will tell GNUPLOT to put the plot in its terminal. + ## If we have no display, we want to use the "dumb" terminal. + ## Octave Link may request that we send the plot as an event. + ## The latter two cases require plotting to a temp file. + + should_plot_to_terminal = ( + !strcmp (term, "dumb") && ( + __event_manager_plot_destination__ () == 0 || + __event_manager_plot_destination__ () == 2 + ) + ); + + if (should_plot_to_terminal) + enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); + __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); + fflush (plot_stream(1)); + endif - if (nargin >= 3 && nargin <= 4) + should_plot_to_temp_file = ( + strcmp (term, "dumb") || + __event_manager_plot_destination__ () == 1 || + __event_manager_plot_destination__ () == 2 + ); + + if (should_plot_to_temp_file) + tmp_file = tempname (); + __gnuplot_draw_to_file__ (h, term, tmp_file); + fflush (plot_stream(1)); + + ## Read the temp file into memory and then delete it + fid = fopen (tmp_file, 'r'); + while (fid < 0) + fprintf (stderr, "🛈 Waiting for plot to finish… ⏳\n"); + pause (0.5); + fid = fopen (tmp_file, 'r'); + endwhile + [a, count] = fscanf (fid, '%c', Inf); + fclose (fid); + unlink (tmp_file); + + ## What to do with the plot data? + if (count > 0) + if (a(1) == 12) + a = a(2:end); # avoid ^L at the beginning + endif + if strcmp (term, "dumb") + puts (a); + else + __event_manager_show_static_plot__ (term, a); + endif + endif + endif + + endif +endfunction + +function __gnuplot_draw_to_file__ (h, term, file, debug_file) ## Produce various output formats, or redirect gnuplot stream to a ## debug file. plot_stream = []; @@ -70,44 +145,6 @@ fclose (fid); endif end_unwind_protect - else # nargin == 1 - ## Graphics terminal for display. - plot_stream = get (h, "__plot_stream__"); - if (isempty (plot_stream)) - plot_stream = __gnuplot_open_stream__ (2, h); - new_stream = true; - else - new_stream = false; - endif - term = gnuplot_default_term (plot_stream); - if (strcmp (term, "dumb")) - ## popen2 eats stdout of gnuplot, use temporary file instead - dumb_tmp_file = tempname (); - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, - term, dumb_tmp_file); - else - enhanced = gnuplot_set_term (plot_stream(1), new_stream, h, term); - endif - __gnuplot_draw_figure__ (h, plot_stream(1), enhanced); - fflush (plot_stream(1)); - if (strcmp (term, "dumb")) - fid = -1; - while (fid < 0) - pause (0.1); - fid = fopen (dumb_tmp_file, 'r'); - endwhile - ## reprint the plot on screen - [a, count] = fscanf (fid, '%c', Inf); - fclose (fid); - if (count > 0) - if (a(1) == 12) - a = a(2:end); # avoid ^L at the beginning - endif - puts (a); - endif - unlink (dumb_tmp_file); - endif - endif endfunction ================================================ FILE: client/.bowerrc ================================================ { "directory": "app/vendor" } ================================================ FILE: client/.gitignore ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . app/vendor/ app/css/ .idea/ .sass-cache/ _old/ config.json ================================================ FILE: client/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: client/COPYING ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: client/Gruntfile.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const fs = require("fs"); const config = require("@oo/shared").config; function getCssTimestamp() { return fs.statSync("dist/css/themes/fire.css").mtime.valueOf(); } function getJsTimestamp() { return fs.statSync("dist/js/app.js").mtime.valueOf(); } function getFileUtf8(filepath) { return function() { return fs.readFileSync(filepath).toString("utf-8"); }; } module.exports = function (grunt) { grunt.loadNpmTasks("grunt-contrib-requirejs"); grunt.loadNpmTasks("grunt-contrib-stylus"); grunt.loadNpmTasks("grunt-contrib-watch"); grunt.loadNpmTasks("grunt-contrib-copy"); grunt.loadNpmTasks("grunt-regex-replace"); grunt.loadNpmTasks("grunt-contrib-uglify"); grunt.loadNpmTasks("grunt-sync"); grunt.initConfig({ pkg: grunt.file.readJSON("package.json"), requirejs: { compile: { options: { baseUrl: "app", mainConfigFile: "app/main.js", out: "dist/js/app.js", name: "js/app", optimize: "uglify2" } } }, stylus: { dev: { options: { compress: false, }, files: [ { expand: true, cwd: "app/styl/themes/" + config.client.theme_collection, src: ["*.styl"], dest: "app/css/themes", ext: ".css" } ] }, dist: { options: { compress: true, }, files: [ { expand: true, cwd: "app/styl/themes/" + config.client.theme_collection, src: ["*.styl"], dest: "dist/css/themes", ext: ".css" } ] } }, uglify: { requirejs: { files: { "dist/js/require.js": ["app/vendor/requirejs/require.js"], "dist/js/runtime.js": ["app/js/runtime.js"], "dist/js/modernizr-201406b.js": ["app/js/modernizr-201406b.js"] } } }, sync: { dist: { files: [{ cwd: "app", src: [ "index.html", "privacy.txt", "compatibility.html", "gdpr.html", "images/**", "!images/logo_collections/**", "!images/logos/**", "!images/flaticons/**", "!images/sanscons/**", "errors/**", "fonts/**", "js/gnuplot/**" ], dest: "dist" }, { cwd: "app/images/logo_collections/" + config.client.theme_collection, src: ["**"], dest: "dist/images/logos" }], verbose: true, compareUsing: "md5" } }, "regex-replace": { appcss: { src: ["dist/js/app.js", "dist/js/runtime.js"], actions: [ { name: "css-timestamp", search: "\\{!css-timestamp!\\}", replace: getCssTimestamp, flags: "g" }, { name: "config-session-payloadMessageDelay", search: "parseInt\\(\"\\d+!config.session.payloadMessageDelay\"\\)", replace: "" + config.session.payloadMessageDelay, flags: "g" }, { name: "config-session-legalTime", search: "parseInt\\(\"\\d+!config.session.legalTime.guest\"\\)", replace: "" + config.session.legalTime.guest, flags: "g" }, { name: "config-session-countdownExtraTime", search: "parseInt\\(\"\\d+!config.session.countdownExtraTime\"\\)", replace: "" + config.session.countdownExtraTime, flags: "g" }, { name: "config-session-countdownRequestTime", search: "parseInt\\(\"\\d+!config.session.countdownRequestTime\"\\)", replace: "" + config.session.countdownRequestTime, flags: "g" }, { name: "config-client-welcome_back_ms", search: "parseInt\\(\"\\d+!config.client.welcome_back_ms\"\\)", replace: "" + config.client.welcome_back_ms, flags: "g" }, { name: "gacode", search: "\\{!gacode!\\}", replace: config.client.gacode, flags: "g" }, { name: "gtagid", search: "\\{!gtagid!\\}", replace: config.client.gtagid, flags: "g" }, { name: "uservoice", search: "\\{!uservoice!\\}", replace: config.client.uservoice, flags: "g" }, { name: "socket_io_path", search: "\\{!socket_io_path!\\}", replace: config.front.socket_io_path, flags: "g" }, { name: "file_history_url", search: "\\{!file_history_url!\\}", replace: config.git.httpUrl, flags: "g" }, ] }, html: { src: ["dist/privacy.txt"], actions: [ { name: "privacy-txt", search: "", replace: getFileUtf8("app/privacy_standalone.txt"), flags: "g" }, { name: "eula-txt", search: "", replace: getFileUtf8("app/eula.txt"), flags: "g" }, ] } }, watch: { stylus: { files: "app/styl/**/*.styl", tasks: ["stylus:dev"] } } }); grunt.registerTask("build-data", function() { grunt.file.write("dist/build_data.json", JSON.stringify({ jsTimestamp: getJsTimestamp(), cssTimestamp: getCssTimestamp(), useDistPaths: true, })); }); grunt.registerTask("default", [ "requirejs", // app.js "uglify", // runtime.js "stylus:dist", "regex-replace:appcss", "sync", "regex-replace:html", "build-data", ]); grunt.registerTask("index", ["sync", "regex-replace:html"]); }; ================================================ FILE: client/README.md ================================================ Octave Online Server: Client ============================ This directory contains the web frontend code for Octave Online. ## Installation Before you can run any of the build scripts, you will need to install Node.JS. Once Node.JS is installed, install the Octave Online Client dependencies like so: npm install npm run bower install Note: **npm** is used to manage the build system dependencies. **Bower** is a front-end dependency manager. Finally, you need to create a file *config.json* in this directory. Use the same format as in the Octave Online Server: Back Server project. If both projects are running on the same host, you can use a symlink. ## Building To build the distribution version of Octave Online Client, simply run Grunt: npm run grunt **Grunt** is a build system, kind-of like **make** or **Ant** but for JavaScript. While you are developing, you can run `grunt watch` to automatically compile the TypeScript and SCSS when changes are made. ================================================ FILE: client/app/.eslintrc.yml ================================================ # Don't inherit ESLint configs: # The JavaScript here runs in the browser environment # and is designed to be ES5 compatible! root: true env: browser: true amd: true extends: 'eslint:recommended' parserOptions: ecmaVersion: 5 rules: indent: - error - tab - SwitchCase: 1 linebreak-style: - error - unix quotes: - error - double semi: - error - always # Allow console.log no-console: - off # Allow extra escapes in regular expressions no-useless-escape: - off ================================================ FILE: client/app/colab.html ================================================ Octave Online Colaboration Demo
================================================ FILE: client/app/eula.txt ================================================ This copy of Octave Online ("the Software Product") and accompanying documentation is licensed and not sold. This Software Product is protected by copyright laws and treaties, as well as laws and treaties related to other forms of intellectual property. Octave Online LLC or its subsidiaries, affiliates, and suppliers (collectively "Octave Online") own intellectual property rights in the Software Product. The Licensee's ("you" or "your") license to download, use, copy, or change the Software Product is subject to these rights and to all the terms and conditions of this End User License Agreement ("Agreement").   Acceptance YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT BY USING, DOWNLOADING, COPYING, OR REPRODUCING THE SOFTWARE. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THIS AGREEMENT, YOU MUST IMMEDIATELY CEASE ALL USE OF THE SOFTWARE.   Restrictions on Alteration You may not modify the Software Product or create any derivative work of the Software Product or its accompanying documentation without written consent of Octave Online. Derivative works include but are not limited to translations. You may not alter any files or libraries in any portion of the Software Product. You may not reproduce the database portion or create any tables or reports relating to the database portion.   Restrictions on Copying You may not copy any part of the Software Product except to the extent that licensed use inherently demands the creation of a temporary copy stored in computer memory and not permanently affixed on storage medium. You may make one archival copy which must be stored on a medium other than a computer hard drive.   Disclaimer of Warranties and Limitation of Liability UNLESS OTHERWISE EXPLICITLY AGREED TO IN WRITING BY OCTAVE ONLINE, OCTAVE ONLINE MAKES NO OTHER WARRANTIES, EXPRESS OR IMPLIED, IN FACT OR IN LAW, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE OTHER THAN AS SET FORTH IN THIS AGREEMENT OR IN THE LIMITED WARRANTY DOCUMENTS PROVIDED WITH THE SOFTWARE PRODUCT.   Octave Online makes no warranty that the Software Product will meet your requirements or operate under your specific conditions of use. Octave Online makes no warranty that operation of the Software Product will be secure, error free, or free from interruption. YOU MUST DETERMINE WHETHER THE SOFTWARE PRODUCT SUFFICIENTLY MEETS YOUR REQUIREMENTS FOR SECURITY AND UNINTERRUPTABILITY. YOU BEAR SOLE RESPONSIBILITY AND ALL LIABILITY FOR ANY LOSS INCURRED DUE TO FAILURE OF THE SOFTWARE PRODUCT TO MEET YOUR REQUIREMENTS. OCTAVE ONLINE WILL NOT, UNDER ANY CIRCUMSTANCES, BE RESPONSIBLE OR LIABLE FOR THE LOSS OF DATA ON ANY COMPUTER OR INFORMATION STORAGE DEVICE.   UNDER NO CIRCUMSTANCES SHALL OCTAVE ONLINE, ITS DIRECTORS, OFFICERS, EMPLOYEES OR AGENTS BE LIABLE TO YOU OR ANY OTHER PARTY FOR INDIRECT, CONSEQUENTIAL, SPECIAL, INCIDENTAL, PUNITIVE, OR EXEMPLARY DAMAGES OF ANY KIND (INCLUDING LOST REVENUES OR PROFITS OR LOSS OF BUSINESS) RESULTING FROM THIS AGREEMENT, OR FROM THE FURNISHING, PERFORMANCE, INSTALLATION, OR USE OF THE SOFTWARE PRODUCT, WHETHER DUE TO A BREACH OF CONTRACT, BREACH OF WARRANTY, OR THE NEGLIGENCE OF OCTAVE ONLINE OR ANY OTHER PARTY, EVEN IF OCTAVE ONLINE IS ADVISED BEFOREHAND OF THE POSSIBILITY OF SUCH DAMAGES. TO THE EXTENT THAT THE APPLICABLE JURISDICTION LIMITS OCTAVE ONLINE'S ABILITY TO DISCLAIM ANY IMPLIED WARRANTIES, THIS DISCLAIMER SHALL BE EFFECTIVE TO THE MAXIMUM EXTENT PERMITTED.   Limitation of Remedies and Damages Your remedy for a breach of this Agreement or of any warranty included in this Agreement is the correction or replacement of the Software Product. Selection of whether to correct or replace shall be solely at the discretion of Octave Online. Octave Online reserves the right to substitute a functionally equivalent copy of the Software Product as a replacement. If Octave Online is unable to provide a replacement or substitute Software Product or corrections to the Software Product, your sole alternate remedy shall be a refund of the purchase price for the Software Product exclusive of any costs for shipping and handling.   Any claim must be made within the applicable warranty period. All warranties cover only defects arising under normal use and do not include malfunctions or failure resulting from misuse, abuse, neglect, alteration, problems with electrical power, acts of nature, unusual temperatures or humidity, improper installation, or damage determined by Octave Online to have been caused by you. All limited warranties on the Software Product are granted only to you and are non-transferable. You agree to indemnify and hold Octave Online harmless from all claims, judgments, liabilities, expenses, or costs arising from your breach of this Agreement and/or acts or omissions.   Governing Law, Jurisdiction and Costs This Agreement is governed by the laws of Missouri, without regard to Missouri's conflict or choice of law provisions.   Severability If any provision of this Agreement shall be held to be invalid or unenforceable, the remainder of this Agreement shall remain in full force and effect. To the extent any express or implied restrictions are not permitted by applicable laws, these express or implied restrictions shall remain in force and effect to the maximum extent permitted by such applicable laws. ================================================ FILE: client/app/fonts/dejavusansmono_book/DejaVuSansMono-demo.html ================================================ DejaVu Sans Mono Book Specimen
AaBb
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
bodyDejaVu Sans Mono Book
bodyArial
bodyVerdana
bodyGeorgia

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.

Lorem Ipsum Dolor

Etiam porta sem malesuada magna mollis euismod

Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

Pellentesque ornare sem

Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.

Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.

Cras mattis consectetur

Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.

Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.

Installing Webfonts

Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.

1. Upload your webfonts

You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.

2. Include the webfont stylesheet

A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:

@font-face{ font-family: 'MyWebFont'; src: url('WebFont.eot'); src: url('WebFont.eot?iefix') format('eot'), url('WebFont.woff') format('woff'), url('WebFont.ttf') format('truetype'), url('WebFont.svg#webfont') format('svg'); }

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

<link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" />

3. Modify your own stylesheet

To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:

p { font-family: 'MyWebFont', Arial, sans-serif; }

4. Test

Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.

================================================ FILE: client/app/fonts/dejavusansmono_book/specimen_files/easytabs.js ================================================ (function($){$.fn.easyTabs=function(option){var param=jQuery.extend({fadeSpeed:"fast",defaultContent:1,activeClass:'active'},option);$(this).each(function(){var thisId="#"+this.id;if(param.defaultContent==''){param.defaultContent=1;} if(typeof param.defaultContent=="number") {var defaultTab=$(thisId+" .tabs li:eq("+(param.defaultContent-1)+") a").attr('href').substr(1);}else{var defaultTab=param.defaultContent;} $(thisId+" .tabs li a").each(function(){var tabToHide=$(this).attr('href').substr(1);$("#"+tabToHide).addClass('easytabs-tab-content');});hideAll();changeContent(defaultTab);function hideAll(){$(thisId+" .easytabs-tab-content").hide();} function changeContent(tabId){hideAll();$(thisId+" .tabs li").removeClass(param.activeClass);$(thisId+" .tabs li a[href=#"+tabId+"]").closest('li').addClass(param.activeClass);if(param.fadeSpeed!="none") {$(thisId+" #"+tabId).fadeIn(param.fadeSpeed);}else{$(thisId+" #"+tabId).show();}} $(thisId+" .tabs li").click(function(){var tabId=$(this).find('a').attr('href').substr(1);changeContent(tabId);return false;});});}})(jQuery); ================================================ FILE: client/app/fonts/dejavusansmono_book/specimen_files/grid_12-825-55-15.css ================================================ /*Notes about grid: Columns: 12 Grid Width: 825px Column Width: 55px Gutter Width: 15px -------------------------------*/ .section {margin-bottom: 18px; } .section:after {content: ".";display: block;height: 0;clear: both;visibility: hidden;} .section {*zoom: 1;} .section .firstcolumn, .section .firstcol {margin-left: 0;} /* Border on left hand side of a column. */ .border { padding-left: 7px; margin-left: 7px; border-left: 1px solid #eee; } /* Border with more whitespace, spans one column. */ .colborder { padding-left: 42px; margin-left: 42px; border-left: 1px solid #eee; } /* The Grid Classes */ .grid1, .grid1_2cols, .grid1_3cols, .grid1_4cols, .grid2, .grid2_3cols, .grid2_4cols, .grid3, .grid3_2cols, .grid3_4cols, .grid4, .grid4_3cols, .grid5, .grid5_2cols, .grid5_3cols, .grid5_4cols, .grid6, .grid6_4cols, .grid7, .grid7_2cols, .grid7_3cols, .grid7_4cols, .grid8, .grid8_3cols, .grid9, .grid9_2cols, .grid9_4cols, .grid10, .grid10_3cols, .grid10_4cols, .grid11, .grid11_2cols, .grid11_3cols, .grid11_4cols, .grid12 {margin-left: 15px;float: left;display: inline; overflow: hidden;} .width1, .grid1, .span-1 {width: 55px;} .width1_2cols,.grid1_2cols {width: 20px;} .width1_3cols,.grid1_3cols {width: 8px;} .width1_4cols,.grid1_4cols {width: 2px;} .input_width1 {width: 49px;} .width2, .grid2, .span-2 {width: 125px;} .width2_3cols,.grid2_3cols {width: 31px;} .width2_4cols,.grid2_4cols {width: 20px;} .input_width2 {width: 119px;} .width3, .grid3, .span-3 {width: 195px;} .width3_2cols,.grid3_2cols {width: 90px;} .width3_4cols,.grid3_4cols {width: 37px;} .input_width3 {width: 189px;} .width4, .grid4, .span-4 {width: 265px;} .width4_3cols,.grid4_3cols {width: 78px;} .input_width4 {width: 259px;} .width5, .grid5, .span-5 {width: 335px;} .width5_2cols,.grid5_2cols {width: 160px;} .width5_3cols,.grid5_3cols {width: 101px;} .width5_4cols,.grid5_4cols {width: 72px;} .input_width5 {width: 329px;} .width6, .grid6, .span-6 {width: 405px;} .width6_4cols,.grid6_4cols {width: 90px;} .input_width6 {width: 399px;} .width7, .grid7, .span-7 {width: 475px;} .width7_2cols,.grid7_2cols {width: 230px;} .width7_3cols,.grid7_3cols {width: 148px;} .width7_4cols,.grid7_4cols {width: 107px;} .input_width7 {width: 469px;} .width8, .grid8, .span-8 {width: 545px;} .width8_3cols,.grid8_3cols {width: 171px;} .input_width8 {width: 539px;} .width9, .grid9, .span-9 {width: 615px;} .width9_2cols,.grid9_2cols {width: 300px;} .width9_4cols,.grid9_4cols {width: 142px;} .input_width9 {width: 609px;} .width10, .grid10, .span-10 {width: 685px;} .width10_3cols,.grid10_3cols {width: 218px;} .width10_4cols,.grid10_4cols {width: 160px;} .input_width10 {width: 679px;} .width11, .grid11, .span-11 {width: 755px;} .width11_2cols,.grid11_2cols {width: 370px;} .width11_3cols,.grid11_3cols {width: 241px;} .width11_4cols,.grid11_4cols {width: 177px;} .input_width11 {width: 749px;} .width12, .grid12, .span-12 {width: 825px;} .input_width12 {width: 819px;} /* Subdivided grid spaces */ .emptycols_left1, .prepend-1 {padding-left: 70px;} .emptycols_right1, .append-1 {padding-right: 70px;} .emptycols_left2, .prepend-2 {padding-left: 140px;} .emptycols_right2, .append-2 {padding-right: 140px;} .emptycols_left3, .prepend-3 {padding-left: 210px;} .emptycols_right3, .append-3 {padding-right: 210px;} .emptycols_left4, .prepend-4 {padding-left: 280px;} .emptycols_right4, .append-4 {padding-right: 280px;} .emptycols_left5, .prepend-5 {padding-left: 350px;} .emptycols_right5, .append-5 {padding-right: 350px;} .emptycols_left6, .prepend-6 {padding-left: 420px;} .emptycols_right6, .append-6 {padding-right: 420px;} .emptycols_left7, .prepend-7 {padding-left: 490px;} .emptycols_right7, .append-7 {padding-right: 490px;} .emptycols_left8, .prepend-8 {padding-left: 560px;} .emptycols_right8, .append-8 {padding-right: 560px;} .emptycols_left9, .prepend-9 {padding-left: 630px;} .emptycols_right9, .append-9 {padding-right: 630px;} .emptycols_left10, .prepend-10 {padding-left: 700px;} .emptycols_right10, .append-10 {padding-right: 700px;} .emptycols_left11, .prepend-11 {padding-left: 770px;} .emptycols_right11, .append-11 {padding-right: 770px;} .pull-1 {margin-left: -70px;} .push-1 {margin-right: -70px;margin-left: 18px;float: right;} .pull-2 {margin-left: -140px;} .push-2 {margin-right: -140px;margin-left: 18px;float: right;} .pull-3 {margin-left: -210px;} .push-3 {margin-right: -210px;margin-left: 18px;float: right;} .pull-4 {margin-left: -280px;} .push-4 {margin-right: -280px;margin-left: 18px;float: right;} ================================================ FILE: client/app/fonts/dejavusansmono_book/specimen_files/specimen_stylesheet.css ================================================ @import url('grid_12-825-55-15.css'); /* CSS Reset by Eric Meyer - Released under Public Domain http://meyerweb.com/eric/tools/css/reset/ */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin: 0;padding: 0;border: 0;outline: 0; font-size: 100%;vertical-align: baseline; background: transparent;} body {line-height: 1;} ol, ul {list-style: none;} blockquote, q {quotes: none;} blockquote:before, blockquote:after, q:before, q:after {content: ''; content: none;} :focus {outline: 0;} ins {text-decoration: none;} del {text-decoration: line-through;} table {border-collapse: collapse;border-spacing: 0;} body { color: #000; background-color: #dcdcdc; } a { text-decoration: none; color: #1883ba; } h1{ font-size: 32px; font-weight: normal; font-style: normal; margin-bottom: 18px; } h2{ font-size: 18px; } #container { width: 865px; margin: 0px auto; } #header { padding: 20px; font-size: 36px; background-color: #000; color: #fff; } #header span { color: #666; } #main_content { background-color: #fff; padding: 60px 20px 20px; } #footer p { margin: 0; padding-top: 10px; padding-bottom: 50px; color: #333; font: 10px Arial, sans-serif; } .tabs { width: 100%; height: 31px; background-color: #444; } .tabs li { float: left; margin: 0; overflow: hidden; background-color: #444; } .tabs li a { display: block; color: #fff; text-decoration: none; font: bold 11px/11px 'Arial'; text-transform: uppercase; padding: 10px 15px; border-right: 1px solid #fff; } .tabs li a:hover { background-color: #00b3ff; } .tabs li.active a { color: #000; background-color: #fff; } div.huge { font-size: 300px; line-height: 1em; padding: 0; letter-spacing: -.02em; overflow: hidden; } div.glyph_range { font-size: 72px; line-height: 1.1em; } .size10{ font-size: 10px; } .size11{ font-size: 11px; } .size12{ font-size: 12px; } .size13{ font-size: 13px; } .size14{ font-size: 14px; } .size16{ font-size: 16px; } .size18{ font-size: 18px; } .size20{ font-size: 20px; } .size24{ font-size: 24px; } .size30{ font-size: 30px; } .size36{ font-size: 36px; } .size48{ font-size: 48px; } .size60{ font-size: 60px; } .size72{ font-size: 72px; } .size90{ font-size: 90px; } .psample_row1 { height: 120px;} .psample_row1 { height: 120px;} .psample_row2 { height: 160px;} .psample_row3 { height: 160px;} .psample_row4 { height: 160px;} .psample { overflow: hidden; position: relative; } .psample p { line-height: 1.3em; display: block; overflow: hidden; margin: 0; } .psample span { margin-right: .5em; } .white_blend { width: 100%; height: 61px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAO1JREFUeNrs3TsKgFAMRUE/eer+NxztxMYuEWQG3ECKwwUF58ycAKixOAGAyAKILAAiCyCyACILgMgCiCyAyAIgsgAiCyCyAIgsgMgCiCwAIgsgsgAiC4DIAogsACIL0CWuZ3UGgLrIhjMA1EV2OAOAJQtgyQLwjOzmDAAiCyCyAIgsQFtkd2cAEFkAkQVAZAHaIns4A4AlC2DJAiCyACILILIAiCzAV5H1dQGAJQsgsgCILIDIAvwisl58AViyAJYsACILILIAIgvAe2T9EhxAZAFEFgCRBeiL7HAGgLrIhjMAWLIAliwAt1OAAQDwygTBulLIlQAAAABJRU5ErkJggg==); position: absolute; bottom: 0; } .black_blend { width: 100%; height: 61px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPJJREFUeNrs3TEKhTAQRVGjibr/9QoxhY2N3Ywo50A28IrLwP9g6b1PAMSYTQAgsgAiC4DIAogsgMgCILIAIgsgsgCILIDIAogsACILILIAIguAyAKILIDIAiCyACILgMgCZCnjLWYAiFGvB0BQZJsZAFyyAC5ZAO6RXc0AILIAIguAyAKkRXYzA4DIAogsACILkBbZ3QwALlkAlywAIgsgsgAiC4DIArwVWf8uAHDJAogsACILILIAv4isH74AXLIALlkARBZAZAFEFoDnyPokOIDIAogsACILkBfZZgaAuMhWMwC4ZAE+p4x3mAEgxinAAJ+XBbPWGkwAAAAAAElFTkSuQmCC); position: absolute; bottom: 0; } .fullreverse { background: #000 !important; color: #fff !important; margin-left: -20px; padding-left: 20px; margin-right: -20px; padding-right: 20px; padding: 20px; margin-bottom:0; } .sample_table td { padding-top: 3px; padding-bottom:5px; padding-left: 5px; vertical-align: middle; line-height: 1.2em; } .sample_table td:first-child { background-color: #eee; text-align: right; padding-right: 5px; padding-left: 0; padding: 5px; font: 11px/12px "Courier New", Courier, mono; } code { white-space: pre; background-color: #eee; display: block; padding: 10px; margin-bottom: 18px; overflow: auto; } .bottom,.last {margin-bottom:0 !important; padding-bottom:0 !important;} .box { padding: 18px; margin-bottom: 18px; background: #eee; } .reverse,.reversed { background: #000 !important;color: #fff !important; border: none !important;} #bodycomparison { position: relative; overflow: hidden; font-size: 72px; height: 90px; white-space: nowrap; } #bodycomparison div{ font-size: 72px; line-height: 90px; display: inline; margin: 0 15px 0 0; padding: 0; } #bodycomparison div span{ font: 10px Arial; position: absolute; left: 0; } #xheight { float: none; position: absolute; color: #d9f3ff; font-size: 72px; line-height: 90px; } .fontbody { position: relative; } .arialbody{ font-family: Arial; position: relative; } .verdanabody{ font-family: Verdana; position: relative; } .georgiabody{ font-family: Georgia; position: relative; } /* @group Layout page */ #layout h1 { font-size: 36px; line-height: 42px; font-weight: normal; font-style: normal; } #layout h2 { font-size: 24px; line-height: 23px; font-weight: normal; font-style: normal; } #layout h3 { font-size: 22px; line-height: 1.4em; margin-top: 1em; font-weight: normal; font-style: normal; } #layout p.byline { font-size: 12px; margin-top: 18px; line-height: 12px; margin-bottom: 0; } #layout p { font-size: 14px; line-height: 21px; margin-bottom: .5em; } #layout p.large{ font-size: 18px; line-height: 26px; } #layout .sidebar p{ font-size: 12px; line-height: 1.4em; } #layout p.caption { font-size: 10px; margin-top: -16px; margin-bottom: 18px; } /* @end */ /* @group Glyphs */ #glyph_chart div{ background-color: #d9f3ff; color: black; float: left; font-size: 36px; height: 1.2em; line-height: 1.2em; margin-bottom: 1px; margin-right: 1px; text-align: center; width: 1.2em; position: relative; padding: .6em .2em .2em; } #glyph_chart div p { position: absolute; left: 0; top: 0; display: block; text-align: center; font: bold 9px Arial, sans-serif; background-color: #3a768f; width: 100%; color: #fff; padding: 2px 0; } #glyphs h1 { font-family: Arial, sans-serif; } /* @end */ /* @group Installing */ #installing { font: 13px Arial, sans-serif; } #installing p, #glyphs p{ line-height: 1.2em; margin-bottom: 18px; font: 13px Arial, sans-serif; } #installing h3{ font-size: 15px; margin-top: 18px; } /* @end */ #rendering h1 { font-family: Arial, sans-serif; } .render_table td { font: 11px "Courier New", Courier, mono; vertical-align: middle; } ================================================ FILE: client/app/fonts/dejavusansmono_book/stylesheet.css ================================================ @font-face { font-family: 'DejaVu Sans Mono'; src: url('DejaVuSansMono-webfont.eot'); src: url('DejaVuSansMono-webfont.eot?#iefix') format('embedded-opentype'), url('DejaVuSansMono-webfont.woff') format('woff'), url('DejaVuSansMono-webfont.ttf') format('truetype'), url('DejaVuSansMono-webfont.svg#dejavu_sans_monobook') format('svg'); font-weight: normal; font-style: normal; } ================================================ FILE: client/app/images/flaticons/download-svg.ai ================================================ %PDF-1.5 % 1 0 obj <>/OCGs[6 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream Adobe Illustrator CS5.1 2015-07-05T03:33:12-07:00 2015-07-05T03:33:12-07:00 2015-07-05T03:33:12-07:00 256 240 JPEG /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgA8AEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FX5V4q7FXYq7FX6laT/xyrL/jBF/xAYqisVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVflXirsVdirsVfqVpP/HKsv+MEX/EBiqKxV2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV+VeKuxV2KuxV+p Wk/8cqy/4wRf8QGKorFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FX5V4q7FXYq7FX6laT/xyrL/jBF/xAYqisVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVedXX/OQ/5N2lzLa3XmSOC5gdo54JLe6R0dDRlZTECCCKEHFVL/oZH8kv+ppg /wCRNz/1SxV3/QyP5Jf9TTB/yJuf+qWKu/6GR/JL/qaYP+RNz/1SxV3/AEMj+SX/AFNMH/Im5/6p Yq7/AKGR/JL/AKmmD/kTc/8AVLFX564q7FXYq7FX6Bad/wA5GfkrFp9rFJ5ogWSOGNXX0bnYhQCP 7rFUR/0Mj+SX/U0wf8ibn/qlirv+hkfyS/6mmD/kTc/9UsVd/wBDI/kl/wBTTB/yJuf+qWKu/wCh kfyS/wCppg/5E3P/AFSxVVtf+ch/ybu7mK1tfMkc9zO6xwQR2907u7miqqiIkkk0AGKvRQaiuKux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV4X/zkP8A848W3ne2l8yeW4kg82wJWWIURL9EGyOd gJgBRHPX7LbUKqviS6tbm0uZbW6ieC5gdo54JFKOjoaMrKaEEEUIOKqWKuxV2KuxV2KuxV2KuxV2 KuxV2KuxVVtbW5u7mK1tYnnuZ3WOCCNS7u7miqqipJJNABir7b/5x4/5x4tvJFtF5k8yRJP5tnSs URo6WCON0Q7gzEGjuOn2V2qWVe6Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8L/AOch /wDnHi28720vmTy3EkHm2BKyxCiJfog2RzsBMAKI56/ZbahVV8SXVrc2lzLa3UTwXMDtHPBIpR0d DRlZTQggihBxVSxV2KuxV2KuxV2KuxV2KuxV2Kqtra3N3cxWtrE89zO6xwQRqXd3c0VVUVJJJoAM Vfbf/OPH/OPFt5ItovMnmSJJ/Ns6ViiNHSwRxuiHcGYg0dx0+yu1Syr3TFXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXhf8AzkP/AM48W3ne2l8yeW4kg82wJWWIURL9EGyOdgJgBRHP X7LbUKqviS6tbm0uZbW6ieC5gdo54JFKOjoaMrKaEEEUIOKqWKuxV2KuxV2KuxV2KuxVVtbW5u7m K1tYnnuZ3WOCCNS7u7miqqipJJNABir7b/5x4/5x4tvJFtF5k8yRJP5tnSsURo6WCON0Q7gzEGju On2V2qWVe6Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8L/wCch/8AnHi2 8720vmTy3EkHm2BKyxCiJfog2RzsBMAKI56/ZbahVV8SXVrc2lzLa3UTwXMDtHPBIpR0dDRlZTQg gihBxVSxV2KuxV2KuxV2Kqtra3N3cxWtrE89zO6xwQRqXd3c0VVUVJJJoAMVfbf/ADjx/wA48W3k i2i8yeZIkn82zpWKI0dLBHG6IdwZiDR3HT7K7VLKvdMVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVeF/8AOQ//ADjxbed7aXzJ5biSDzbAlZYhREv0QbI52AmAFEc9fstt Qqq+JLq1ubS5ltbqJ4LmB2jngkUo6OhoyspoQQRQg4qpYq7FXYq7FVW1tbm7uYrW1iee5ndY4II1 Lu7uaKqqKkkk0AGKvtv/AJx4/wCceLbyRbReZPMkST+bZ0rFEaOlgjjdEO4MxBo7jp9ldqllXumK uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvC/+ch/+ceLbzvbS +ZPLcSQebYErLEKIl+iDZHOwEwAojnr9ltqFVXxJdWtzaXMtrdRPBcwO0c8EilHR0NGVlNCCCKEH FVLFXYqq2trc3dzFa2sTz3M7rHBBGpd3dzRVVRUkkmgAxV9t/wDOPH/OPFt5ItovMnmSJJ/Ns6Vi iNHSwRxuiHcGYg0dx0+yu1Syr3TFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXhf/OQ//OPFt53tpfMnluJIPNsCVliFES/RBsjnYCYAURz1+y21Cqr4kurW 5tLmW1uonguYHaOeCRSjo6GjKymhBBFCDirrW1ubu5itbWJ57md1jggjUu7u5oqqoqSSTQAYq+2/ +ceP+ceLbyRbReZPMkST+bZ0rFEaOlgjjdEO4MxBo7jp9ldqllXumKuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVZNPDBE808ixQxgtJI5CqoHUknYDFWNzfmn+WMMjRTeb9F jlXZkfUbRWB9wZMVWf8AK2Pys/6nLQ/+4lZ/9VMVTTR/N3lTW246NrVhqbUrSzuYbg0BIr+7ZvDF U2xVB6vrejaNZm+1i/ttNslIVrq7lSCIM3QF5Cq1PzxVWs72zvbWK7sp47m1mXnDcQuskbqe6upK kfLFXiP/ADkP/wA48W3ne2l8yeW4kg82wJWWIURL9EGyOdgJgBRHPX7LbUKqu/5x4/5x4tvJFtF5 k8yRJP5tnSsURo6WCON0Q7gzEGjuOn2V2qWVexTeZ/LUGsR6LNq1lFrMwBi0x7iJblwRUFYS3qHb wGKplirsVSDU/wAwvIOlXTWmqeZdKsLtCQ9vdX1tDIpHUFHdWGKr9I8+eR9Zufquj+YtM1K6PSC0 vLeeT/gY3Y9sVTe7vLSztpLq8njtraEcpZ5mWONFHdmYgAfPFVHS9X0nVrNL3Sr2DULKT+7urWVJ omp4PGWU/fiqLxV2KuxV2KuxV2KuxV2KuxV2KuxV2KpX5i80+XPLdg2oa/qVvplmtaS3Mix8iBXi gJq7f5KgnFXj8X/OYH5ZT+brbRoY7kaRMxjl8wTKIoEc04H0m/e+kf2nYKV/lpuFXuUckcsayRsH jcBkdSCrKRUEEdQcVXYq+Df+ckvzZ13zZ541PQo7p4vLei3L2dvYxsRHLLbsUknlA2di6nhXovTu SqkVp/zjz+c91bRXMPlW6EUyh09RoYnodxySSRXU+xGKqv8A0Lh+dv8A1K1x/wAjbb/qrirG9f8A J/n7yHqVrNq+n3mhXob1LK6NUPOM/aimjPGq/wCS2Kvtr/nG78wfMfnj8uV1HzBHW/srqSx+u8eI ukiSNhNTpy/ecWoKVHzAVeSf85xXlz9a8pWXqEWvC8mMQPwmSsKhiO5A2HzPjirN/wDnDK9ubj8p ruGZy8dnrFzBbKf2IzBBMVH/AD0lY/Tir3jFXYq/Nj8wtZ1I/mn5h1b12F/DrN1LFOD8SNDct6fH w4cRT5Yq/SfFXyX/AM5A/wDOT+pvfX/k/wAkyNZwW0j22pa2ppNI6ErJHbEfYQEEGT7R/ZoNyq8O 8rflP+ZPm+zfUtA0K51Cz5lWu/hSNnB+IK8rIHIPWlcVW+afyv8AzH8lwwahr2i3WmQM49G8+FkW QGqj1YmdUb+WpB8MVTXzL+eHnjzP+X9v5N1y5N7DbXUdwuoOx+sSRxo6rDOf92gM4YMd6jevUKvb v+cGry5ez842bSE20MmnzRRdlklW4V2HuwiT7sVfUmKvJfzV/wCckfJn5e65baLPDLq2oMQ2ow2b JW1iIqpfnRWkbYiOo23JG1VWQeR/zu/LPzoI49G1mJb+Tb9GXZ+r3XL+VY5Kep/zzLDFWdYq7FXY q7FXYq7FXYqxHzz+bHkDyPCzeYtXht7njySwjPq3T+HGFKvvXq1B74q+bPzA/wCczvMN/wCrZ+St PXSbY1VdSvAs10R/MkW8MZ+fPFXz9r3mPXvMGoPqOuahcalev1nuZGkYCteK8j8Kjso2GKpbir6H /wCccv8AnI2TyxJb+UfN1wX8uORHp2oyElrJidkc94D/AMJ/q9FX2XHJHLGskbB43AZHUgqykVBB HUHFX5+/85D/AJdav5Q/MfVbia3caPrN1NfaXd7tGyzuZHi5fzxM5Uqd6UPeuKs9/Lz/AJzJ8zaW sNj5ysV1qzQBDqFvSK9AApV1P7qU/wDAHxOKvpXyJ+b/AOXvniMDy/q8Ut3Sr6dN+5ul8f3T0ZgP FKj3xVkWu+XdB1+wOn65p9vqdkWD/V7qJJU5DowDg0I8Riqvpum6dpljDYadbRWdjbLwt7WBFjiR R2VFAAGKvBP+cyPIk2seSrLzTaKWn8uyMt0gqa2t0URmp4pIqfQSe2KvO/8AnDb8whpfmq98m3kv Gz1xfrFgGPwreQLVlH/GWEH6UUd8VfZGKsS/NbzzB5H8g6v5icj6xbQlLCNt+d1L8EK07jmQW9gc VfBv5VeTr38wPzJ0zSJmeZLy4Nzq07GrfV0Pq3Dsx/aYVUE/tEYq/Ri/neCwuZ0pziid1r0qqkiu KvzU8h6fba5+YPl3TtSBmttU1azt70E/E6XFyiSb+JDHFX6WWVlZ2NpDZ2UEdtaW6COC3hUJGiKK KqKoAUAdhiqzU9M07VdPuNO1K2ju7G6QxXFtMoeN0bqGU4q+Iv8AnJD8iLH8u7m11rQ7gv5f1SZo Us5SWltpgpfgHP20Kg8SdxShr1xVnn/ODH/Tbf8Abr/7HMVZt/zkP/zkPbeSLaXy35blSfzbOlJZ RR0sEcbO43BmINUQ9PtNtQMq+JLq6ubu5lurqV57md2knnkYu7u5qzMxqSSTUk4qpgkGo2IxV6V5 G/5yI/NTyf6cNrqzalpyUA0/U63MQA2ARiRLGB4I4Htir6F8jf8AOZPkjVRHbearObQLtqBrmOt1 aE+JKD1Ur4cCB/Nir3TQ/MOg69Yrf6JqFvqVm/Se1lSVa+BKk0PiDviqYYq7FXlv5gf85Jflh5N9 W3a//TGrR1X9HaaVmKsNqSS1EUdD9oFuQ/lxV80fmB/zlh+ZPmb1LXSHXy1pj7COyYtdFf8ALuiF Yf8APNUxV4xPPNPM808jSzSEtJI5LMzHqSTuTiqnirsVdirsVfUX/OJn5secWu4/JV5YXer6Au1p qMUbSfo/vwmfoID2qaqem3RV9Sa1oWja5p0um6zYwahYTf3ltcxrIh8DRgaEdiNxir5//MD/AJwz 8sal6l35Mv20W6NSNPui09oT4K+80f8Aw/yxV82+dvyl/MbyFcCXW9LmtoI3Bh1S3Pq23IEFSs8e yGvQNQ4qzT8uv+cqvzH8rSQ22rz/AOJNHWivDesfrKrXf07ndyf+MnIfLFX2d5G87aD518tWnmHQ 5TJZXQIKOAssUimjxSqC3F1PXf3FQRiqaatpdlq2l3el30Ymsr6GS3uYjuGjlUow39jir84fMWka 7+Wv5kXFmjmLU/L18stnOQRzWNhLBLTb4ZE4tTwOKv0O8learDzZ5T0vzHY/7zanbpMErUo52kjP vG4ZT7jFXyv/AM5m/mCb/wAx2Hkm0kra6Qou9RCnY3c6/u1b/jHCa/7PFWb/APOG/wCXh0ryteec r2Lje643oWHIbrZQtuw7j1ZQfoVTir6A1j/jk3v/ADDy/wDEDir84fyn/wDJp+Tf+25pv/UZHir9 KcVdir5t/wCc29QsR5R8v6eZ0+vPqDXC23IeoYkhdGk49ePJwK4q8U/J78xvNnkryd51l8saVcXd /ffo+NtUjiMsFgiC6rLLQMA7c/3fL4agk9KFV5bdXVzd3Mt1dSvPczu0k88jF3d3NWZmNSSSaknF VLFXYq7FXYqmOheYte0C+W/0TUbjTbxdhPaytExHWhKkVHsdsVe6eRv+cyvO2lCO281WUOv2q0Bu o6Wt2B4koDE9P9RSe5xV9DeRv+ch/wAq/OHpw2mrLp+oyU/3HalS2lqf2VZiYpD7I5OKvz0xV2Ku xV2KuxVnPkH8lfzG88sj6HpTjT2NDql1WC0ArQkSMPjp3EYY+2Kvpf8AL7/nDrybpHp3fm26fX75 aE2icoLJW8CAfVkp7sAe64q960vSdL0myjsNLs4bGyhFIra2jWKNR7IgAxViGl/nf+Vuqea28q2O vwzayGMaR8ZBFJIvWOOdlETt7K2/auKs5xVbLFFNG0UqLJE4KujgMrA9QQdiMVfH/wDzl1+WfkLy wmkazoFvFpep6nPLHdabb/DDJGq8jMkXSPi1FPEBTXpXFWR/84OXt6+k+bbJyfqUE9lNAO3qzJMs tPfjDHir6fxV8t/85o/l56ltpvnyyj+OEjTtW4jqjEtbytT+VuSE+6jFUl/5xb/ObTvLPlTzPouu S/6NpNvJrWmoTQuAAk1uhP7TuYyg8WY4q8W0bTtf/Mz8yorZn56r5kv2kuZqErGJWMk0lN/giTka eApir9G9G0iw0bSLLSdPjENjYQx21tGO0cShV6d6DFUTPCk8EkEn93KrI9NjRhQ4q/NnzP5a80/l r56+p3aNa6rpFyl1p91T4ZBFJzguIj0ZWKA+x2O4OKvqjyR/zmJ5Cv8ARY282LLo+sx0W4SGGSe3 kNPtxFA7KD/K3TxOKqXn7/nMXyVY6K48mJJq2tTArC1xDJBbQn+eTnwd/ZV69yMVfInmPzLrvmXW LjWNcvJL/UrpuUs8p39lUCiqo7KoAHbFX0v/AM4Mf9Nt/wBuv/scxV7J54/5x9/KzzjzlvtHSx1B 6k6jp1LWYk/tOFHpyH3kRsVfPXnn/nDPzlppkufKV/DrlqN1s5qWt2B4AsfRenjzX5Yq8J1/yz5i 8u3xsdd0250y7G4huomiJHivIDkPcbYqlmKuxV2KuxV2KuxV2KtgEkACpOwA61xV65+X3/OMP5n+ bvTubi0/QGlPQ/XNSDRyMp7x2/8AettuOQVT/Nir6X/L7/nFz8svKYiuby2PmHVo6E3eoKGhDjvH bbxj258yPHFXr6IiIqIoVFACqBQADYAAYq3iq2WNZYniavF1KtQkGhFDQjFXwH+a/wDzjx538i6p LJZ2k+seXWcmz1S2jMjKpOy3CICY3HjTiex7BVhA86+fLP8A0Ya9qlv6e3oi7uE4/wCx5imKu/5W D59/6mXVf+k24/5rxVE6T5S/MXzxqafUdP1HW7yei/WnWWVQDuDJPJ8Crv1ZgMVfcn5DflSfy48k rpt1Ik2s30n1vVJY6lBIVCrEhPVY1FK9zU4q9IxVJ/OHljT/ADT5X1Py9qAraanbvA7d0YiqSL/l I4DD3GKvzZ80+WNZ8ra/faDrEDW9/ZSNFKpBCsAfhdCQOSOPiVu4xV9Pf84b/lhc2kN7591W2MTX SfVNDEgIJhJrPOAezkKiN7N2OKvqDFXYqw78z/yq8rfmLoR0zWouFxFVrDUogv1i3fxRiN1b9pDs fnQhV8yX/wDzhN+YiXTrYa3pFxagn05Z3uYJCK7co0gnVf8Agziqy0/5wn/MhrmNbzWdGhtif3ss Ml1LIq+KxtbxBj7FxiqP/wCch/yl8r/lx+U2hado6Ga7n1VW1DU5QPWuHFtLStPsov7KDYe5qSqm 3/ODH/Tbf9uv/scxV9VYq7FUDrOhaJrdi9hrNhb6jZSfat7qJJUPvxcEV98VeG+ef+cOfIer+pc+ WLqby9eNUiDe6tCev2Hb1Er7PQfy4q+e/PP/ADjf+a3lH1JptLOq6dHv9f0wm4Tj4tGAJkp3LJT3 xV5gQQSCKEbEHrXFWsVdirsVdirYJBBBoRuCOtcVel+Sf+ci/wA1/KRjit9XbU7BKf6BqdbqOgps rsRMgp2VwMVe/wDkj/nM3yZqXp23mvT59DuTQNdw1urUnYFjxAmT5cG+eKvdPLvmzyz5lsvrugap banbbcntpVk4k9nUHkh9mAOKprirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVfJX/OZH5j+X NVGmeT9LuEvL3TblrrU5YiGjhcIY1h5A0L/Gxcfs7Drtiqbf84OadPHpXm7UmB9C5nsraM02526T O+/yuFxV9P4q7FUq8xebPLPlqy+u6/qltpltvxe5lWPkR2RSeTn2UE4q8L87/wDOZvkzTfUtvKmn z65cioW7mra2oO4DDkDM/wAuC/PFXgHnb/nIv81/NpkiuNXbTLB6/wCgaZW1joa7M6kzOKdmcjFX mhJJJJqTuSetcVaxV2KuxV2KuxV2KuxVF6Xq+q6TepfaVeT2F7F/d3NtI8Mq/J0KsMVe1eSf+cvv zJ0P07fXUg8yWK0BM49C6AHYTxjifm8bH3xV795I/wCcqPyq8y+nBeXj+X9QegMGpAJFy2rxuVLR cd+rlT7Yq9dtrm3uoI7i2lSe3lUNFNGwdGU9CrLUEYqqYq7FXYq7FXYq7FXYq7FXYq7FXzR/zmBr n5mabNow0C4v7Ly20MjXlzp7SRg3If7E8kVCAEAKhjQ7+Gyr5h/5WD59/wCpl1X/AKTbj/mvFWx5 58/XP7geYNVm9T4fS+uXLcq9uPM1xVmv5Vf848+efPeoxy3FrNo/l9WButVuo2QsvdbeN+LSsfH7 I7nsVX2/5U8reWPInlaHSNLVbHSLBC8k0zgEk7yTTSNQcmO5PTwoNsVefed/+cqPyq8tepBZ3j+Y NQSoEGmgPFy3pyuWKxcduqFj7Yq8B87f85ffmTrnqW+hJB5bsWqAYB690QexnkHEfNI1PvirxXVN X1XVr177Vbye/vZf7y5uZHmlb5u5ZjiqExV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVkflH8xfPHk+ f1vLes3OnfFyaCN+UDnbd4H5RP0/aXFXvXkn/nNbVYPTtvOejJexigbUNOPpTU8WgkJjc/6roPbF Xv8A5J/Ov8s/OYSPRdbh+uv/ANK65P1e5r4COTjz/wBhyGKs4xV2KuxV2KuxV2KuxV2KuxV2KsH8 6/nX+Wfk31I9a1uD69HsdOtj9YueX8rRx8uFf8viMVeAedv+c1tVn9S28maMllGahdQ1E+rNTxWC MiND/rO49sVeC+bfzE87+b5/V8x6zc6iAeSQSPxgQ+KQpxiXr2XFWOYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq9H8k/8AOQX5q+UPTisdZe9sE6afqNbqGg/ZUsfVQe0brir3/wAk /wDOaPlO+9O383aZNo85oGvbWt1be7MgAmT5Kr4q918s+cvKnmi0+t+XtWtdTgABc28iuyV7SJ9t D7MAcVTnFXYq7FXYqk3mbzl5U8r2n1vzDq1rpkBBKG4kVGenaNPtufZQTirwrzt/zmj5TsfUt/KO mTaxOKhb26ra23syoQZn+TKmKvAPO3/OQX5q+b/UivtZeysH66fp1bWGh/ZYqfVce0jtirzjFXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqidO1PUtMu473TbuayvIjW K5t5GilU+KuhVh9+KvZvJX/OXH5n6D6cGsGHzHYpsVux6VyFHZbiMbn3kVzir3/yT/zlf+VfmL04 NRuJPLt+2xi1AD0Cf8m5SsYHvJwxV3nb/nK/8q/LvqQadcSeYr9dhFp4HoA/5Vy9IyPePnirwDzr /wA5cfmfr3qQaOYfLli+wW0Hq3JU9muJBsfeNUOKvGdR1PUtTu5L3Urua9vJTWW5uJGllY+LO5Zj 9+KobFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX6Hedv8AnHz8qvN/qS32jJZX79dQ06lrNU/t MFBic+8iNir5/wDO3/OF/myw9S48o6nDrMA3WyuqWtz7KrkmF/mWTFXhPmXyf5p8sXn1PzBpVzpk 5rwW5jZFenUo5+Fx7qTiqT4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FU48teT/NPme8+p+X9KudTnFOa20bOqV6F3HwoPdiMVe7eSf+cL/Nl/6dx5u1OHRoDu 1la0urn3VnBEKfMM+KvoDyT/AM4+flV5Q9OWx0ZL2/TpqGo0upqj9pQwESH3jRcVejYq7FUNqOma bqdnJZalaQ31nLtLbXMaSxMP8pHDKfuxV4v52/5xF/LPXvUn0X1vLd824NqfWtiT3a3kP4I6DFXg Hnb/AJxU/NXy56k9jaJ5hsF3E2nEtMB/lWzUlJ9o+eKvILq1urS4e2uoXt7iI8ZIZVKOp8GVgCMV UsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqra2t1d3CW1rC9xcSnjHDEpd2P gqqCTir1/wAk/wDOKn5q+Y/TnvrRPL1g25m1ElZiP8m2WsoPtJwxV7/5J/5xF/LPQfTn1r1vMl8u 5N0fRtgR3W3jP4O7jFXtGnaZpumWcdlptpDY2cW0VtbRpFEo/wAlECqPuxVE4q7FXYq7FXYq7FXY qx7zZ+Xvknzdb+h5k0a11IAcUllSkyD/AIrmTjKn+xYYq8F87f8AOFOkXHqXHkzWXsZTuthqIM0N fBZ4x6iD5o5xV8/+dvyT/MzyZ6kmtaJN9Rj3Oo2o+sW1PFpI+XD/AGfE4qwbFXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYqznyT+Sf5mec/Tk0XRJvqMm41G6H1e2p4rJJx5/wCw5HFX0B5J/wCc KdIt/TuPOesvfSjdrDTgYYa+DTyD1HHyRDir3ryn+Xvknyjb+h5b0a100EcXliSszj/iyZ+Ur/7J jirIcVdirsVdirsVdirsVdirsVdirsVdirsVec+dv+cfPyq83+pLfaMllfv11DTqWs1T+0wUGJz7 yI2Kvn/zt/zhf5ssPUuPKOpw6zAN1srqlrc+yq5Jhf5lkxV4T5l8n+afLF59T8waVc6ZOa8FuY2R Xp1KOfhce6k4qk+KuxV2KuxV2KuxV2KuxV2KuxVOPLXk/wA0+Z7z6n5f0q51OcU5rbRs6pXoXcfC g92IxV7t5J/5wv8ANl/6dx5u1OHRoDu1la0urn3VnBEKfMM+KvoDyT/zj5+VXlD05bHRkvb9Omoa jS6mqP2lDARIfeNFxV6NirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVQ2o6Zpup2cll qVpDfWcu0ttcxpLEw/ykcMp+7FXi/nb/AJxF/LPXvUn0X1vLd824NqfWtiT3a3kP4I6DFXgHnb/n FT81fLnqT2NonmGwXcTacS0wH+VbNSUn2j54q8gurW6tLh7a6he3uIjxkhlUo6nwZWAIxVSxV2Ku xV2Kqtra3V3cJbWsL3FxKeMcMSl3Y+CqoJOKvX/JP/OKn5q+Y/TnvrRPL1g25m1ElZiP8m2WsoPt JwxV7/5J/wCcRfyz0H059a9bzJfLuTdH0bYEd1t4z+Du4xV7Rp2mabplnHZabaQ2NnFtFbW0aRRK P8lECqPuxVE4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqx7zZ+Xvk nzdb+h5k0a11IAcUllSkyD/iuZOMqf7FhirwXzt/zhTpFx6lx5M1l7GU7rYaiDNDXwWeMeog+aOc VfP/AJ2/JP8AMzyZ6kmtaJN9Rj3Oo2o+sW1PFpI+XD/Z8TirvJP5J/mZ5z9OTRdEm+oybjUbofV7 anisknHn/sORxV9AeSf+cKdIt/TuPOesvfSjdrDTgYYa+DTyD1HHyRDir3ryn+Xvknyjb+h5b0a1 00EcXliSszj/AIsmflK/+yY4qyHFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX/2Q== 1 False False 40.000000 40.000000 Points OCRAStd OCR A Std Regular Open Type Version 2.036;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895 False OCRAStd.otf Cyan Magenta Yellow Black Default Swatch Group 0 Black RGB PROCESS 0 0 0 application/pdf download-svg xmp.did:A60C28F253206811AFFDE64AE370C81B uuid:4bcda3da-31ce-014d-aa74-633cb22b5a0a xmp.did:A10C28F253206811AFFDE64AE370C81B proof:pdf xmp.iid:A50C28F253206811AFFDE64AE370C81B xmp.did:A50C28F253206811AFFDE64AE370C81B xmp.did:A10C28F253206811AFFDE64AE370C81B saved xmp.iid:A10C28F253206811AFFDE64AE370C81B 2015-07-05T03:31:01-07:00 Adobe Illustrator CS5.1 / saved xmp.iid:A20C28F253206811AFFDE64AE370C81B 2015-07-05T03:31:06-07:00 Adobe Illustrator CS5.1 / saved xmp.iid:A30C28F253206811AFFDE64AE370C81B 2015-07-05T03:31:36-07:00 Adobe Illustrator CS5.1 / saved xmp.iid:A40C28F253206811AFFDE64AE370C81B 2015-07-05T03:31:53-07:00 Adobe Illustrator CS5.1 / saved xmp.iid:A50C28F253206811AFFDE64AE370C81B 2015-07-05T03:32:34-07:00 Adobe Illustrator CS5.1 / saved xmp.iid:A60C28F253206811AFFDE64AE370C81B 2015-07-05T03:33:09-07:00 Adobe Illustrator CS5.1 / Document Adobe PDF library 9.90 endstream endobj 3 0 obj <> endobj 8 0 obj <>/Resources<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 12 0 R/TrimBox[0.0 0.0 40.0 40.0]/Type/Page>> endobj 9 0 obj <>stream HTQ=O1 +<o=* !3TUhI.w(Rg9ۈndܼ\&^?+|̔$ ,]V O\N-՟`[P endstream endobj 12 0 obj <>stream 8;Xq'^qapf3f%0/"TSQ-!(?P`>l~> endstream endobj 13 0 obj [/Indexed/DeviceRGB 255 14 0 R] endobj 14 0 obj <>stream 8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn 6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 6 0 obj <> endobj 15 0 obj [/View/Design] endobj 16 0 obj <>>> endobj 5 0 obj <> endobj 17 0 obj <> endobj 18 0 obj <>stream H}@ >Wjhs.ê5މIX wA`qn~IK2q;%`,o͢ňXw;͉yϪO`:A endstream endobj 11 0 obj <> endobj 10 0 obj <> endobj 19 0 obj <> endobj 20 0 obj <>stream %!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 15.0 %%AI8_CreatorVersion: 15.1.0 %%For: (Shane Carr) () %%Title: (download-svg.svg) %%CreationDate: 7/5/15 3:33 AM %%Canvassize: 16383 %%BoundingBox: 285 377 326 416 %%HiResBoundingBox: 285.5 377.75 325.5 415.25 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 11.0 %AI12_BuildNumber: 39 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 285.5 376.5 325.5 416.5 %AI3_TemplateBox: 305.5 396.5 305.5 396.5 %AI3_TileBox: 17.5 40.5 593.5 774.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 2 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: 211.75 449.75 8 1631 842 26 0 0 43 134 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:0 0 %AI7_GridSettings: 283.4646 10 283.4646 10 1 0 0.8 0.8 0.8 0.9 0.9 0.9 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 21 0 obj <>stream %%BoundingBox: 285 377 326 416 %%HiResBoundingBox: 285.5 377.75 325.5 415.25 %AI7_Thumbnail: 128 120 8 %%BeginData: 5810 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD38FF27FD0FF8FD70FFFD10F8A8FD6FFF27FD0FF8A8FD6FFFFD10 %F8A8FD6FFF27FD0FF8FD70FFFD10F8A8FD6FFF27FD0FF8FD70FFFD10F8A8 %FD6FFF27FD0FF8FD70FFFD10F8A8FD6FFF27FD0FF8A8FD6FFFFD10F8A8FD %6FFF27FD0FF8FD70FFFD10F8A8FD6FFF27FD0FF8FD70FFFD10F8A8FD6FFF %27FD0FF8FD70FFFD10F8A8FD6FFF27FD0FF8A8FD57FF52F8F827F827F827 %F827F827F827F827F827F827F827F827FD11F827F827F827F827F827F827 %F827F827F827F827F827F852FD41FF27FD3CF827FD43FF27FD3AF827FD45 %FF27FD38F852FD47FF27FD36F827FD49FF27FD34F852FD4BFF27FD32F827 %FD4DFF27FD30F827FD4FFF27FD2EF827FD51FF27FD2CF852FD53FF27FD2A %F827FD55FF27FD28F852FD57FF27FD26F827FD59FF27FD24F852FD5BFF27 %FD22F827FD5DFF27FD20F827FD5FFF27FD1EF827FD61FF27FD1CF852FD63 %FF27FD1AF827FD65FF27FD18F852FD67FF27FD16F827FD69FF27FD14F852 %FD6BFF27FD12F827FD6DFF27FD10F827FD6FFF27FD0EF827FD71FF27FD0C %F852FD73FF27FD0AF827FD75FF27FD08F852FD77FF27FD06F827FD79FF27 %FD04F852FD7BFF27F8F827FD7DFF2727FD62FFA87D7D527D527D527D527D %7DFD07FFA87D7DFD07FFA8FD04FF7D52FD09FFA87D527D527D7DFD04FFA8 %52FD45FF52FD0CF827FD06FF27F8F8A8FD0AFFA8F8F827FD07FF7D27FD06 %F827A8FFA8F8F827FD43FF27FD0FF8FD04FFA8F8F8F87DFD0AFF52F8F8F8 %A8FD05FF52FD0AF87D27F8F8F8A8FD36FF5227A8FD08FFA8FD10F852FFFF %FFA8F8F8F852FD0AFF52F8F8F8FD05FF27FD10F8FD08FF5227A8FD29FF7D %52F8F8F827A8FD07FF52F8F8F87DFD08A87DF8F8F87DFFFFFFA8F8F8F87D %FD0AFF52F8F8F8A8FFFFFF52FD04F852FD04A827FD07F8A8FD06FF27F8F8 %F82752A8FD24FFA827FD06F827FD07FF52F8F8F8FD0AFFA8527DFD04FFA8 %F8F8F852FD0AFF52F8F8F8FD04FF27F8F8F8A8FD06FF7DFD06F8FD06FF52 %FD07F8527DFD20FF5227FD09F827A8FD05FF52FD04F852A8FD0FFF27F8F8 %F8A8FD08FFA8F8F8F852FFFFFFA8F8F8F852FD08FF7D27FD04F8A8FD04FF %27FD0BF852A8FD1AFFA852FD0DF827FD06FFFD06F852A8FD0DFFA8F8F8F8 %52FD08FF27F8F8F8A8FFFFFFA8F8F8F87DFD09FFA827F8F8F8FD04FF52FD %0EF827A8FD16FFA827FD10F827A8FD04FFA8FD07F8277DFD0BFFA827F8F8 %F8FD07FFA8F8F8F827FD04FFA8F8F8F87DFD0AFF27F8F8F8A8FFFF27FD11 %F82752A8FD11FF7D27FD13F827A8FD05FF5227FD06F8277DA8FD09FF7DF8 %F8F87DFD06FF52F8F8F8A8FD04FFA8F8F8F852FD0AFF52F8F8F8FFFF52FD %15F8527DFD0CFFA852FD17F827A8FD05FFA87DFD08F852FD09FFF8F8F827 %FD05FFA8F8F8F827FD05FFA8F8F8F87DFD0AFF52F8F827A852FD19F8527D %FD07FFA852FD1AF827A8FD07FF7D52FD07F8A8FD07FF7DF8F8F87DFD04FF %52F8F8F87DFD05FFA8F8F8F852FD0AFF52F8F8F827FD1CF8277DA8FFFF7D %27FD1BF82752FD0AFFA827FD06F8A8FD06FFA8FD04F8FFFFFFA827F8F8F8 %FD06FFA8F8F8F87DFD0AFFFD04F85227FD1EF827A8FD1BF8277DFD0EFFA8 %7DFD04F852FD07FF52F8F8F87DFFFF7DF8F8F87DFD06FFA8F8F8F852FD08 %FFA8FD05F8FFFFA85227FD36F8277DFD12FFA8F8F8F827FD07FFA8F8F8F8 %52FFFFFD04F8A8FD07FFFD04F87DFD06FF7DFD06F8A8FFFFFFA87D27FD32 %F852A8FD08FFA8FD0CFF52F8F8F8FD08FF52F8F8F8FFA8F8F8F852FD08FF %52FD04F852FFA8A8A852FD07F8FD07FFA852FD2EF852A8FD09FF52F8F8A8 %FD0AFFF8F8F827A8FD07FF7DF8F8F82752F8F8F8A8FD09FF27FD10F8A8FD %09FF7D27FD28F8277DFD0BFFA8FD04F8FD0952FD04F827FD09FF27FD06F8 %52FD0BFF52FD0AF82752F8F8F8FD0DFF5227FD23F8277DFD0EFF27FD10F8 %A8FD09FFA8FD06F8A8FD0CFF52FD08F852FF52F8F8F8A8FD0EFFA827FD1F %F8527DFD11FF27FD0EF87DFD0BFF27FD04F827FD0EFFA87D7D527D527D7D %FFFF52F8F8F8FD11FFA87D27FD1AF852FD15FF5227FD09F82752A8FD0CFF %7D27F8F8F8FD19FF52F8F8F8A8FD13FFA852FD18F87DA8FD4CFF52F8F8F8 %FD14FFA85227FD1AF852A8FD49FF27F8F827A8FD10FFA85227FD1FF8277D %A8FD45FFA827F8F827FD0FFF7D27FD25F8277DA8FD41FF7DFD04F852FD0C %FF7D52FD2AF82752A8FD35FFA8FD085227F8F827F8F8A8FD09FFA87DFD30 %F82752FD32FFA8FD0DF827A8FD07FFA87D27FD35F8527DFD2FFF7DFD0CF8 %27A8FD06FFA82727FD3AF8527DFD2DFF5227FD07F827277DFD05FFA85227 %FD3FF8277DA8FD39FF7D27FD45F8277DA8FD33FF7D52FD4AF82752A8FD2E %FFA852FD50F82752A8FD28FFA87DFD56F8527DFD24FFA827FD5BF8527DFD %1EFFA852FD60F8277DA8FD19FF5227FD65F8277DA8FD13FF7D27FD6AF827 %52A8FD0EFFA852FD70F82752A8FD08FFA87D27FD75F8527DFFFFFFA87D27 %FD7BF85252FDFCF8FDFCF8FD47F8A85227FD7BF8527DFFFFA87D27FD76F8 %277DFD08FFA87D27FD70F82752FD0EFF7D52FD6CF827A8FD13FF7D52FD66 %F8277DA8FD17FFA85227FD61F87DA8FD1DFFA85227FD5BF8527DFD22FFA8 %7D27FD56F8277DFD28FFA87D27FD50F82752FD2EFF7D52FD4BF82752A8FD %33FF7D52FD46F8277DA8FD37FFA85227FD41F852A8FD3DFFA85227FD3BF8 %52A8FD42FFA87D27FD36F8277DFD48FFA87DFD31F82752A8FD4DFF7D52FD %2CF827A8A8FD52FF7D52FD26F8277DFD58FFA85227FD21F87DA8FD5DFFA8 %5227FD1BF852A8FD62FFA87D27FD16F8277DFD68FFA87D27FD10F82752FD %6EFF7D52FD0CF827A8FD73FF7D52FD06F8277DFD78FFA852F8F87DA8FD3C %FFFF %%EndData endstream endobj 22 0 obj <>stream %AI12_CompressedDataxݽ~:0zn{bK*tһ'(&6wG\dz}γO`yFfFM(&*'2I j"-GO&O?KQ4Ei$Q =l·#!75NH|;ZLx` 3 !NGUJ[&&ǤrL9W.cn|Ul0}Wt4/tޟM膗mUI/Yt 唟-:%&LQ*kO8??^i hzUt%q$Mx@)*'Gu9 .ȗ)_3'w .|L/]~!oZUG_}*`Ea>ı%ԩ>n|0â%RcBRr [(d6:#R2I*+Oo6˲_r‹wbTr! hߜp返Wnpˉ0Y.U NxJFp5g=cL`l)BKٴđS6CXfJ}fe83.f|6OhD" ?Sتv ٠<Hp%ޙ33z^_}/LY ^&gE7OLe0EDb*"x`4pL/h3a1࿘J&4i?́fK~L}v꿂8ь_N~W,y |?#QͬRqO;7@"pҜLEl? F<2.1U07w2F4Oڧ/gSR~o+03sG@As'Zt1`SX}Ȝ:אIs"~Dnt%7㾒[-p0w)my"\_?jт}p9t-`0fxw@4wq+-+SY_Pt_'F}$ gysg^RvV0ϒ/yf4k`[YpܨD_|b .0#n2}}1`S( }G 2Xb7J9vXR^L$* VoޟβGϴ'L>_ESCɴ쀉jw9!3YG-QXOf_/"!z-$7GM%~!x}Uu J]1O9V'Aw&܌w h jL`(q!0R4WcuРUq) 0iЕѯG0bR1?&ckP<0ϼ0y#iK6 HsG0&j栂el",|K)Zjǜ݄̀`p3䦼?c#Kէ&b6K}/SCf3hPbS<Jh'mODžϏ7#0?dyE5u-'7b{ lX|/f6N˽ϫ }S^#:g`vzԽ5oѨg٤w|@ouZ>zs |,A Va+?(?@ ^:|F ~!#i?s*n?x8{0 mVoz^ އM*mCmXC`QؐIr=]8(x_X!{ ߑʿ];ٙI`=BSi`npqE.1?p0۬GN ml:HƇor_D2]&/HWX} 'd򐇮b~fqss =N?b:,mxPL:no班A:5`:!Z gTG+PFW0.)ﯡ@'Yޟ͑(J 3eJ9D 梦 GsL#[{ ̵8ٞ: r,y0/0HZp&}VtOԚms?cY EadP[XiКzp$jHWN: xSo3@0FΊ&Y?-*U'}jvĭ)X%_X.`ȖKRZXZRh  Yb 1"r0-]=n/K yqO$AɎbX^Zm.'TI oOJ;j_n%hl :ΩP@}/o0; rnM {'[{X.M;iz+D^˂4Ҥ_J]1z0itBܘy"W4Ru>\pXj*qL3lv"䍊s${Iɿ4瀡2/i-Jrԇj-j6Z.˅~=@Y \ ?` rh< 1X`ʛ ~0ZN1oVtrY5j  棤ddH\0K)')ɫs+˅ѕ*H{N]Z}~eQU6-u-ڶ)\:.:ڶgqqZaR]&A'v=0(2IB)˜Q^CƴфZ[> Gi_y~RXxye =3`y+x dNTՙbkٟ!$]?=M`t:QuZP"*:vk ^Jo"D))a*=rFsmM4RmɁr҄YwYT0,]Hny.@h97fjZ#.n""Ud ʰe!glZ`.OF@aZt `$#I}Fv t ؆ Lf7#GJFPR)$tOȘf`d.~ m.eAa)BS&?,Y㽦h.en^S5^/!D'D,ѥl^F/ Ai*mq nF8;X)77KŃp0Q Yj8L P1!۩$Ny7e;jfD*yFgNAn3U^͸ ݵm;@MʆyCAuoBj'#3p"&5F:Uu78.|]g@$yQQ4lF; UkSB1xi=s|.&܉`ClȻt漫_@ ?"`ri[`BA#JYƘ>Jjȭ %b_qSfld ;d*ha!-&J|@F )vA[0'ؙ:Kp!VCr{ue`8DNG6).N_Qa ڣُki6SnB KD}EAX̍`N({xR@ttXv`ac6NکЃKvSZt?]g/?[v[з-'oi ~Πs3ZX=-"JIS9wPzfn}td/u!B8 ѸɂD?ƥXv w\?hVLtS\AŋCWLG'7Ҧ!w@㙴{ޑ9U|ot5N(gKئK:SM9yOBppRcK+'5%oGP[# MF!6@k]7texn-idN)BSə5JJO<52ˤKhf:{ cgw -87@ތM=;I(Fyx \ɵy¹xn=GKxP܊)<{|0@v|Š/9qtQ {@RT5`Q5] a&0cgL^anf+Ֆzӹ?B.~MYbbc:B#6~f@->ZGmn]C[Gw鍋cmCm֨M`Q#&vY'XuG訒J<κj7Eߤ6H )/#CҢ,(LY;bA6VA€Cdh,7K6Oҽ&#WIRYX_(j2qOSWZ^6S"cW(G:!Nn&ž HΆW2O&[Cޯ8[D3g~ GY$~N?+ ՛Z8Wj5#,s~^j77A(ȴWg@A# d_Z{':r`dl3@%Il IRU_] ]?b g-~z(kK[}C nc+I1WH!a;1,ǎ _3ڃkz9]T_ָ}sտاCi:vp{ Bhr\Y)uVJKq0S-fy? v?e|MYkrWTB3xm=BL|Vijۣ1ؗۧS٩iH>`MOb\5_p%QLK\ 9 Z?`, $GD1_D)`T 0|3rs 6oWuMT vҹ2OYyUAĆ+([ y_l2s^\[}IF*˧ښ(u{ң_L+ Bƞ[.x15&!}׉HsיHB),#Zii[R!EBΖB )ĂoH0C*.?H/xjmG`q醙VXOlzm;ҭ]"mm &6|z N|O!Ҩ[Pne˜`p4Bkv+v߻! -ߌTd=q{IDZ`a{(c=N`y*Ra4\[HOOl^H|V i)|{kCE VHvV#S"bZ4GكWiu<}g DkAu摞vR_(>c=g?w4n4~vRhQo΁tqӑ >ЦO(a"MZ5IBimaBkoR8/IO4,ΌHf)kXO:RܯrUQ:i">.vޅ2ujzjd!-CKS z;`vb5/`0bC4.Ά[ħI2'{V;}(Qykjk;;I~)Y(vR槽SLSb Z/~ A_μR>]&>蠟^NjX4=|$ݟ礧;]2vO|4}"=h4fʑ=t<%MOo?[nH5S+lFeױZb/5Ez}&֬ۚi;"l\ $NKOIߚx<QW j{rY(*7ŭtPA*$v&vlJH, 6K5|m7; Si陈_)6.DPaIH8l7 ah.t#4c"Op51 >]@4);$9HX c}#"dp@D ~0)GtpB0|IE>)=4ޗ -?+vw1ViiY) ;l.EEtM+mydplL\A K/ԋR׀3D)*w0HU!_inp{K ]viRGqc${fOsAY/NL» ֽG19Ɂ|[V܈(6 ?T篻>v!4}E?OGI,`&Ub%= ` W$ KC!vr%d,E ص;IWЕq.STyag 18N@1|B0`|յ%_h,m[=4c_1+:%"fA = ͨ7˂|!/ۂ4i{e6ĺtХ)QW^(1@:u ]affpr4,<_3Aɜ_`UR3*!'MAV7H1DدY-šul6$VAÀ lU8ت>m\Lq˹NN͇f[s(ijՍ`oMBvOӡ3uDdkW\UGLj4qYXG=ruaO.i.<$&C|:<`rlvB^:0JK!gL+Nt@'/LAr ?l뾾_'HaLU_oRK .!3X9~=hq-qF&X3~ b\ⶻnw-:/5%kLO?ӕ_VFz4 _ȁ*.9a>\>#ٲ:&E禇p[V aU߀0qa\Izy[صvSQ#Ho; K`BkRYsVy>j8ƍ(g2uW EMPTwmN)Q`Y⬡ ׍0P/ ) Nz>#0tY1h1DQD< f!+t^Xf`Gy.ѴUٺ6 I4k.,  BYq8 W#) Y%t/8C{%1[=7[V/_j`_-&f'! |҆* 5SPI¸á3PY?A!lM'peljYŠuaԆ:=Ƥ֍֛.}샅ַ_tW-gͻK˪ u{ Wb}oE۽jCg]$Р ? u#HPa)O)J,.E#KWgBn@bv߿7J52|Z̚AgZ99  :b: :<[p: :շFs![p: :lAO9tt([p9ts, אA,̡sΠC{! !ìnYxr&)؅K޺{.]TӴG^9n~mޗQ{:쌯nXN{v5j:>B ݜ(3>'`qXtC>94g#O4 +O|핃Y,xsldK%A"uQ [m=vBN~xsD+iid1tɐ :pq1k1:ei%IWNIڄGyd֌Rp#ɝu<2~t} q𐆶Gƒ-G<2T'"=.9t{k4tc7Nc,gb8?L$ux3N0r%& mv\2wC%t6[-sЬ5BQ9aN3KՋٖz]7JE漓6l"9cEAw·[#C>SzHgs#(1k|8C4Nxp{G匸Mù|8ÉEjpXfYb"(cL$ɶb"־4άkPNm)L.R$] Dud5}LjϮ%92EH<ޯ #̫2ce4]8-m6`!#8I3tZJ`mZ0kҦq &9_亡2RS[`c&],RF*ADJmh\.~H}LF*T/~F*ThӖ.-򥩞 eqehJcNC=H­4Rya_F*~J+p6^ gܟ=9(OvJ|cUin7c˶6 Qy)f;6a X6@۩<{r9xY9Q_X5.nO,հ5>ۃeY 7o;cfy{Tx?7n ߞ֪~Ve`\UGˊɘeuwIJ 9x]eLv_]g in ӣSۀ!"EEMH ;XV#!t@J0\rL¼pd7XjZjhJۊ8%&z|{SR]~{'|%%8eM3["ҝs~8 !]?6._ZDgkcU`QO )ٵ s*hGn!zˉY:)Bnll&pF6jXPt ;s[p[{d%9#6wPx*y½ԩC$]!ѝ܊9K9꾹{j1ݥF8Ʌ᮴=dӭMgPkqn.~5|~6].ݪl:;_}:t(k@B6݊c69+7MgK7MgKG!׮V`\6"OFNE7MgKGm~l:kdKil:;t6DCMgZSxl:ml:zMge>P6JMcl:MgX6MgwZb@6x0^7Mr2l:wl:;b26MgKGǷ0o <]De-RiA Z6׫tiOy6]l]i {N%C$$t5,(Y@z&u%ZQ@Q]x0R @<]v} Q8ɡ2AdB-fN%ӎ(,] ;{jYN.ssEiRX]*W).*@9>Y/t\4UfTTe\=0kSlBwks\ 6pU.6V]V<ȵO$ˣ!:X\{y"EIx2dSKo 77R+)Vs!mm?OzoRT ɥ14)\fLY/ l9gH̹8y;P`qH[<iz)VlR+Hoݓ6wMCd@'% s{g.ScJs\+ &fS;Q=FYsk2l5EA*f>0'6zH)I*b ';UImrOl6U9t]*zΡUĩTf^BМL7O)SGOC/Ӽ0sEB,QZBMtOQZS~bo]ҒkW]uZ3e}Bq)˴l,(ūWUdLQ>9g@Y(/dcn(>(Gɢ|T+)ʇ{`N)$rK &W[nM EY׏naCP~V\ol9{w3P+sMs fճ*ꟲ΍(~"T|8=){h&̜Դ_( \i[έC~Gҿ V֓\~BS;b!b.vdXQFc Ld;r{2 `&驚XKv1C9J6w>7-0N KЀXa>T[B& hZ7`Y3`YwǙw@akrZi=GA*Tl\O{2 'W>b6ܙ$an\DsbEF#|,dIaS*|ǀ"ZEY@׃80a^DC-dEmzj H[ROf'z<=D৔)߲6n~?b Ҹ/kڗ)/н@t2H0{P X/+p|?+|dc;-b`Hf.:C3ED] ʒdKw%`2@d\#n˹A+Z}ޕQAUx!ɨZH8"գ6eQHKfڵP,s$;Dʢ8,#e(>D$}O4o&-,'֬JOЋɑT|+uwTK5^[@P|=g\ JKVsj釖V9UUXnږi"P7_S$[emgt2\&hv,I9rm>|.b1S K+`r ~uo Yߨ(g2fbNIs^Lxmh̎ql0ᛧޯH% jdxԌpj3Ð7nl\b= mooW+lu- [O 46ZzꈈU&3.gjzpg"Zw((I\̠)0+ָdvbN?v^ͧX:t]NLE =MxcaWAC+U!W|j1Soo ~TG"׍vg:0t$5TUY(Ǜ\d]3mGmV@i:\,=C_qse ЏwtT soHbHK oC9g7Y[{ ::~p] 0Fg|_&1c=K3Icqh|毆D̋}ekm6\`[2O_\ʾoUϕI>X]ktWY>>m\S/c%\IS[EּO#oKyX8 ?DžQVA:]}J'VfV $~^_(ݐf# ؊!]O&<KohmhDlzp\OA CЂ]ޛb*Qtsg 0^%wN/tOpzLw]C9fZEF ' LtD/vb%}oPcg %X;qz%l#A+eE[]xW&p,VZu֓DTj/l`ŀU 3Vd Dqc{gk:dBA`DmКAykEnf݅M^׊KMJxpM>fBV\\:;xǫ]kMZ> Ğ#ǁ$ ?k8z>Kxn{WW4`L qZzp'u o;Ѡ~~٬ $d7Gv\[M/JxaWr芙?V„c`IEګfs^ohK,m- kU<jKvQQA%Rkd>l9=a፧m?1%f07}x؇8R+ݯw*}j~Pu!BNPnVi~'ʄ!ĚRMaV4Ŋʿ<^m&M;/w-<eѴNoL:9HNGpZ,䃨6ފߤ^Nn5 =~[I[P`yMot7ׇM.aG'cy>/ibE[H/xⶲ-I40RR) @YeG]YTi[ |8 TIe?crIuk{˗+Vqܫ@$B:Q].EDFJ0RF[rwL\kN²b6&a87zK@k*'BZA&^b gH+mU:@^6 MzuÕSX r#•Uﯾ#8W: p\:W@FJ"ru93/ɳqT>0,"=;W̽ҁN*OJ]z|O3c˨.mUӛ Qas18`l$x ^Tmݣ8J?A9iO'e{9ϟP4. -"S$JDkJo\ZM ;R|go׃ckUu3N{k7Lܒ$k s {D'#4$iL{AO7i{R IG#i#g^'[Y`79ga 2 {l ,  b_~f9+q==S9If)-.S~L[;9)|_~t\1 Cd(AԿJq{怫Of{6iLj"x8ޗs%[œz ^ᙈoyOMa1c/*YI:,M9{_syP9#1n DX[yk tzT.z;`;4a\?SR}_M˗׮i5'T;(l86 q5|!қJ26c ،)c`3no l@1Y}YYbەiK {N;I6P΂@-/YIѱP5 Z1Zt1jPe.TaŌQ|VۓȌf@w"p,ݝԑ"҄Tx֕:/+Eic PAt\#;NUԟqZs=Ql>BHhDP*7#)8X؅U 6YSYm`x!@i P!X}E@;*U"%=i<jcqWf TaޏYʱ@-#C,ͫ(-{zZ 1>8P<w.0M+/uIx/0W@&|!j̭Voネ]U5tC'|kוǜFպ`Rc֡=2i;kfofQV%0cr5X;dF;"oݤ] 2) @;H9{3Sz߱F&B1Oyc*S~XkIgRB(JRmsʼSVoy/zۢX^h }ѱnVEx[4oy/zۢ}!OEx[2/Ex[}+dW-Z9M/x[t12dN"E;S䣍 $0`ԓ ͻD$dWn`nG, >?H'P ܍<%w/'% 1^lk큽ëa. bRZB̽I|!Apl`:Џсk1% W]q٤"$6k] }WT<Jз lB!K_,Af^,-tџԸ9OE3_胩 3N/ß/N5M4O+%_߮|lcӠ|/f6N%6Ϣ84|9~X _%slʟ)ˤ,(&Y@K tԀ[,;M%πt*Y24 ̟)T> KI6*M`2,Es:6SRF|-.34m|6k =klt[!7oyd |y$|O!/XqSd6wG3l1&#N;YyU^`&[M ߼̈́[%KC xEmU q endstream endobj 7 0 obj [6 0 R] endobj 23 0 obj <> endobj xref 0 24 0000000000 65535 f 0000000016 00000 n 0000000144 00000 n 0000024056 00000 n 0000000000 00000 f 0000025673 00000 n 0000025488 00000 n 0000054881 00000 n 0000024107 00000 n 0000024463 00000 n 0000027021 00000 n 0000026908 00000 n 0000024760 00000 n 0000024927 00000 n 0000024975 00000 n 0000025557 00000 n 0000025588 00000 n 0000025861 00000 n 0000026126 00000 n 0000027095 00000 n 0000027269 00000 n 0000028289 00000 n 0000034291 00000 n 0000054904 00000 n trailer <<4F0049F23B184AF8970FCB3F165C9FAF>]>> startxref 55082 %%EOF ================================================ FILE: client/app/images/logo_collections/official/favicon_package/README.md ================================================ # Your Favicon Package This package was generated with [RealFaviconGenerator](https://realfavicongenerator.net/) [v0.16](https://realfavicongenerator.net/change_log#v0.16) ## Install instructions To install this package: Extract this package in <web site>. If your site is http://www.example.com, you should be able to access a file named http://www.example.comfavicon.ico. Insert the following code in the `head` section of your pages: *Optional* - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker) ================================================ FILE: client/app/images/logo_collections/official/favicon_package/browserconfig.xml ================================================ #da532c ================================================ FILE: client/app/images/logo_collections/official/favicon_package/site.webmanifest ================================================ { "name": "Octave Online", "short_name": "Octave Online", "icons": [ { "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ff4b33", "background_color": "#ff4b33", "display": "standalone" } ================================================ FILE: client/app/images/logo_collections/server/favicon_package/README.md ================================================ # Your Favicon Package This package was generated with [RealFaviconGenerator](https://realfavicongenerator.net/) [v0.16](https://realfavicongenerator.net/change_log#v0.16) ## Install instructions To install this package: Extract this package in <web site>. If your site is http://www.example.com, you should be able to access a file named http://www.example.comfavicon.ico. Insert the following code in the `head` section of your pages: *Optional* - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker) ================================================ FILE: client/app/images/logo_collections/server/favicon_package/browserconfig.xml ================================================ #da532c ================================================ FILE: client/app/images/logo_collections/server/favicon_package/site.webmanifest ================================================ { "name": "Octave Online Server", "short_name": "Octave Online Server", "icons": [ { "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ad928e", "background_color": "#ad928e", "display": "standalone" } ================================================ FILE: client/app/images/sanscons/license.txt ================================================ SANSCONS ICONS Source: http://somerandomdude.com/work/sanscons/ Copyright: P.J. Onori License: http://creativecommons.org/licenses/by-sa/3.0/us/ ================================================ FILE: client/app/js/ace-adapter.js ================================================ /* eslint-disable */ /* From https://github.com/firebase/firepad/blob/master/lib/ace-adapter.coffee Copyright (c) 2015 Firebase, https://www.firebase.com/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ define(["ace/ace", "ot"], function(ace, ot){ function bind(fn, me){ return function(){ return fn.apply(me, arguments); }; } var slice = [].slice; function ACEAdapter(aceInstance) { this.onCursorActivity = bind(this.onCursorActivity, this); this.onFocus = bind(this.onFocus, this); this.onBlur = bind(this.onBlur, this); this.onChange = bind(this.onChange, this); var ref; this.ace = aceInstance; this.aceSession = this.ace.getSession(); this.aceDoc = this.aceSession.getDocument(); this.aceDoc.setNewLineMode('unix'); this.grabDocumentState(); this.ace.on('change', this.onChange); this.ace.on('blur', this.onBlur); this.ace.on('focus', this.onFocus); this.aceSession.selection.on('changeCursor', this.onCursorActivity); this.aceSession.selection.on('changeSelection', this.onCursorActivity); if (this.aceRange == null) { this.aceRange = ((ref = ace.require) != null ? ref : require)("ace/range").Range; } } ACEAdapter.prototype.ignoreChanges = false; ACEAdapter.prototype.grabDocumentState = function() { this.lastDocLines = this.aceDoc.getAllLines(); return this.lastCursorRange = this.aceSession.selection.getRange(); }; ACEAdapter.prototype.detach = function() { this.ace.removeListener('change', this.onChange); this.ace.removeListener('blur', this.onBlur); this.ace.removeListener('focus', this.onCursorActivity); this.aceSession.selection.removeListener('changeCursor', this.onCursorActivity); this.callbacks = {}; }; ACEAdapter.prototype.onChange = function(change) { var pair; if (!this.ignoreChanges) { pair = this.operationFromACEChange(change); this.trigger.apply(this, ['change'].concat(slice.call(pair))); return this.grabDocumentState(); } }; ACEAdapter.prototype.onBlur = function() { if (this.ace.selection.isEmpty()) { return this.trigger('blur'); } }; ACEAdapter.prototype.onFocus = function() { return this.trigger('focus'); }; ACEAdapter.prototype.onCursorActivity = function() { return setTimeout(((function(_this) { return function() { return _this.trigger('cursorActivity'); }; })(this)), 0); }; ACEAdapter.prototype.operationFromACEChange = function(change) { var action, delete_op, delta, insert_op, ref, restLength, start, text; if (change.data) { delta = change.data; if ((ref = delta.action) === 'insertLines' || ref === 'removeLines') { text = delta.lines.join('\n') + '\n'; action = delta.action.replace('Lines', ''); } else { text = delta.text.replace(this.aceDoc.getNewLineCharacter(), '\n'); action = delta.action.replace('Text', ''); } start = this.indexFromPos(delta.range.start); } else { text = change.lines.join('\n'); start = this.indexFromPos(change.start); } restLength = this.lastDocLines.join('\n').length - start; if (change.action === 'remove') { restLength -= text.length; } insert_op = new ot.TextOperation().retain(start).insert(text).retain(restLength); delete_op = new ot.TextOperation().retain(start)["delete"](text).retain(restLength); if (change.action === 'remove') { return [delete_op, insert_op]; } else { return [insert_op, delete_op]; } }; ACEAdapter.prototype.applyOperationToACE = function(operation) { var from, index, j, len, op, range, ref, to; index = 0; ref = operation.ops; for (j = 0, len = ref.length; j < len; j++) { op = ref[j]; if (ot.TextOperation.isRetain(op)) { index += op; } else if (ot.TextOperation.isInsert(op)) { this.aceDoc.insert(this.posFromIndex(index), op); index += op.length; } else if (ot.TextOperation.isDelete(op)) { from = this.posFromIndex(index); to = this.posFromIndex(index - op); range = this.aceRange.fromPoints(from, to); this.aceDoc.remove(range); } } return this.grabDocumentState(); }; ACEAdapter.prototype.posFromIndex = function(index) { var j, len, line, ref, row; ref = this.aceDoc.$lines; for (row = j = 0, len = ref.length; j < len; row = ++j) { line = ref[row]; if (index <= line.length) { break; } index -= line.length + 1; } return { row: row, column: index }; }; ACEAdapter.prototype.indexFromPos = function(pos, lines) { var i, index, j, ref; if (lines == null) { lines = this.lastDocLines; } index = 0; for (i = j = 0, ref = pos.row; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { index += this.lastDocLines[i].length + 1; } return index += pos.column; }; ACEAdapter.prototype.getValue = function() { return this.aceDoc.getValue(); }; ACEAdapter.prototype.getCursor = function() { var e, e2, end, ref, ref1, start; try { start = this.indexFromPos(this.aceSession.selection.getRange().start, this.aceDoc.$lines); end = this.indexFromPos(this.aceSession.selection.getRange().end, this.aceDoc.$lines); } catch (_error) { e = _error; try { start = this.indexFromPos(this.lastCursorRange.start); end = this.indexFromPos(this.lastCursorRange.end); } catch (_error) { e2 = _error; console.log("Couldn't figure out the cursor range:", e2, "-- setting it to 0:0."); ref = [0, 0], start = ref[0], end = ref[1]; } } if (start > end) { ref1 = [end, start], start = ref1[0], end = ref1[1]; } return { position: start, selectionEnd: end }; }; ACEAdapter.prototype.setCursor = function(cursor) { var end, ref, start; start = this.posFromIndex(cursor.position); end = this.posFromIndex(cursor.selectionEnd); if (cursor.position > cursor.selectionEnd) { ref = [end, start], start = ref[0], end = ref[1]; } return this.aceSession.selection.setSelectionRange(new this.aceRange(start.row, start.column, end.row, end.column)); }; ACEAdapter.prototype.setOtherCursor = function(cursor, color, clientId) { var clazz, css, cursorRange, end, justCursor, ref, self, start; if (this.otherCursors == null) { this.otherCursors = {}; } cursorRange = this.otherCursors[clientId]; if (cursorRange) { cursorRange.start.detach(); cursorRange.end.detach(); this.aceSession.removeMarker(cursorRange.id); } start = this.posFromIndex(cursor.position); end = this.posFromIndex(cursor.selectionEnd); if (cursor.selectionEnd < cursor.position) { ref = [end, start], start = ref[0], end = ref[1]; } clazz = "other-client-selection-" + (color.replace('#', '')); justCursor = cursor.position === cursor.selectionEnd; if (justCursor) { clazz = clazz.replace('selection', 'cursor'); } css = "." + clazz + " {\n position: absolute;\n background-color: " + (justCursor ? 'transparent' : color) + ";\n border-left: 2px solid " + color + ";\n}"; this.addStyleRule(css); this.otherCursors[clientId] = cursorRange = new this.aceRange(start.row, start.column, end.row, end.column); self = this; cursorRange.clipRows = function() { var range; range = self.aceRange.prototype.clipRows.apply(this, arguments); range.isEmpty = function() { return false; }; return range; }; cursorRange.start = this.aceDoc.createAnchor(cursorRange.start); cursorRange.end = this.aceDoc.createAnchor(cursorRange.end); cursorRange.id = this.aceSession.addMarker(cursorRange, clazz, "text"); return { clear: (function(_this) { return function() { cursorRange.start.detach(); cursorRange.end.detach(); return _this.aceSession.removeMarker(cursorRange.id); }; })(this) }; }; ACEAdapter.prototype.addStyleRule = function(css) { var styleElement; if (typeof document === "undefined" || document === null) { return; } if (!this.addedStyleRules) { this.addedStyleRules = {}; styleElement = document.createElement('style'); document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement); this.addedStyleSheet = styleElement.sheet; } if (this.addedStyleRules[css]) { return; } this.addedStyleRules[css] = true; return this.addedStyleSheet.insertRule(css, 0); }; ACEAdapter.prototype.addEventListener = function(event, cb) { if(!this.callbacks) this.callbacks = {}; if(!this.callbacks[event]) this.callbacks[event] = []; this.callbacks[event].push(cb); }; ACEAdapter.prototype.trigger = function(event) { var args = (2 <= arguments.length) ? slice.call(arguments, 1) : []; if(this.callbacks && this.callbacks[event]){ for (var i = this.callbacks[event].length - 1; i >= 0; i--) { this.callbacks[event][i].apply(this, args); } return true; } else return false; }; ACEAdapter.prototype.applyOperation = function(operation) { if (!operation.isNoop()) { this.ignoreChanges = true; } this.applyOperationToACE(operation); return this.ignoreChanges = false; }; ACEAdapter.prototype.registerUndo = function(undoFn) { return this.ace.undo = undoFn; }; ACEAdapter.prototype.registerRedo = function(redoFn) { return this.ace.redo = redoFn; }; ACEAdapter.prototype.invertOperation = function(operation) { return operation.invert(this.getValue()); }; return ACEAdapter; }); ================================================ FILE: client/app/js/ace-extras.js ================================================ /* eslint-disable */ // Additional Ace classes define("ace/ext/language_tools",["require","exports","module","ace/snippets","ace/autocomplete","ace/config","ace/autocomplete/text_completer","ace/editor"],function(e,t,n){var r=e("../snippets").snippetManager,i=e("../autocomplete").Autocomplete,s=e("../config"),o=e("../autocomplete/text_completer"),u={getCompletions:function(e,t,n,r,i){var s=e.session.getState(n.row),o=t.$mode.getCompletions(s,t,n,r);i(null,o)}},a={getCompletions:function(e,t,n,i,s){var o=r.snippetMap,u=[];r.getActiveScopes(e).forEach(function(e){var t=o[e]||[];for(var n=t.length;n--;){var r=t[n],i=r.name||r.tabTrigger;if(!i)continue;u.push({caption:i,snippet:r.content,meta:r.tabTrigger&&!r.name?r.tabTrigger+"⇥ ":"snippet"})}},this),s(null,u)}},f=[a,o,u];t.addCompleter=function(e){f.push(e)};var l={name:"expandSnippet",exec:function(e){var t=r.expandWithTab(e);t||e.execCommand("indent")},bindKey:"tab"},c=function(e,t){h(t.session.$mode)},h=function(e){var t=e.$id;r.files||(r.files={}),p(t),e.modes&&e.modes.forEach(h)},p=function(e){if(!e||r.files[e])return;var t=e.replace("mode","snippets");r.files[e]={},s.loadModule(t,function(t){t&&(r.files[e]=t,t.snippets=r.parseSnippetFile(t.snippetText),r.register(t.snippets,t.scope),t.includeScopes&&(r.snippetMap[t.scope].includeScopes=t.includeScopes,t.includeScopes.forEach(function(e){p("ace/mode/"+e)})))})},d=e("../editor").Editor;e("../config").defineOptions(d.prototype,"editor",{enableBasicAutocompletion:{set:function(e){e?(this.completers=f,this.commands.addCommand(i.startCommand)):this.commands.removeCommand(i.startCommand)},value:!1},enableSnippets:{set:function(e){e?(this.commands.addCommand(l),this.on("changeMode",c),c(null,this)):(this.commands.removeCommand(l),this.off("changeMode",c))},value:!1}})}),define("ace/snippets",["require","exports","module","ace/lib/lang","ace/range","ace/keyboard/hash_handler","ace/tokenizer","ace/lib/dom"],function(e,t,n){var r=e("./lib/lang"),i=e("./range").Range,s=e("./keyboard/hash_handler").HashHandler,o=e("./tokenizer").Tokenizer,u=i.comparePoints,a=function(){this.snippetMap={},this.snippetNameMap={}};(function(){this.getTokenizer=function(){function e(e,t,n){return e=e.substr(1),/^\d+$/.test(e)&&!n.inFormatString?[{tabstopId:parseInt(e,10)}]:[{text:e}]}function t(e){return"(?:[^\\\\"+e+"]|\\\\.)"}return a.$tokenizer=new o({start:[{regex:/:/,onMatch:function(e,t,n){return n.length&&n[0].expectIf?(n[0].expectIf=!1,n[0].elseBranch=n[0],[n[0]]):":"}},{regex:/\\./,onMatch:function(e,t,n){var r=e[1];return r=="}"&&n.length?e=r:"`$\\".indexOf(r)!=-1?e=r:n.inFormatString&&(r=="n"?e="\n":r=="t"?e="\n":"ulULE".indexOf(r)!=-1&&(e={changeCase:r,local:r>"a"})),[e]}},{regex:/}/,onMatch:function(e,t,n){return[n.length?n.shift():e]}},{regex:/\$(?:\d+|\w+)/,onMatch:e},{regex:/\$\{[\dA-Z_a-z]+/,onMatch:function(t,n,r){var i=e(t.substr(1),n,r);return r.unshift(i[0]),i},next:"snippetVar"},{regex:/\n/,token:"newline",merge:!1}],snippetVar:[{regex:"\\|"+t("\\|")+"*\\|",onMatch:function(e,t,n){n[0].choices=e.slice(1,-1).split(",")},next:"start"},{regex:"/("+t("/")+"+)/(?:("+t("/")+"*)/)(\\w*):?",onMatch:function(e,t,n){var r=n[0];return r.fmtString=e,e=this.splitRegex.exec(e),r.guard=e[1],r.fmt=e[2],r.flag=e[3],""},next:"start"},{regex:"`"+t("`")+"*`",onMatch:function(e,t,n){return n[0].code=e.splice(1,-1),""},next:"start"},{regex:"\\?",onMatch:function(e,t,n){n[0]&&(n[0].expectIf=!0)},next:"start"},{regex:"([^:}\\\\]|\\\\.)*:?",token:"",next:"start"}],formatString:[{regex:"/("+t("/")+"+)/",token:"regex"},{regex:"",onMatch:function(e,t,n){n.inFormatString=!0},next:"start"}]}),a.prototype.getTokenizer=function(){return a.$tokenizer},a.$tokenizer},this.tokenizeTmSnippet=function(e,t){return this.getTokenizer().getLineTokens(e,t).tokens.map(function(e){return e.value||e})},this.$getDefaultValue=function(e,t){if(/^[A-Z]\d+$/.test(t)){var n=t.substr(1);return(this.variables[t[0]+"__"]||{})[n]}if(/^\d+$/.test(t))return(this.variables.__||{})[t];t=t.replace(/^TM_/,"");if(!e)return;var r=e.session;switch(t){case"CURRENT_WORD":var i=r.getWordRange();case"SELECTION":case"SELECTED_TEXT":return r.getTextRange(i);case"CURRENT_LINE":return r.getLine(e.getCursorPosition().row);case"PREV_LINE":return r.getLine(e.getCursorPosition().row-1);case"LINE_INDEX":return e.getCursorPosition().column;case"LINE_NUMBER":return e.getCursorPosition().row+1;case"SOFT_TABS":return r.getUseSoftTabs()?"YES":"NO";case"TAB_SIZE":return r.getTabSize();case"FILENAME":case"FILEPATH":return"ace.ajax.org";case"FULLNAME":return"Ace"}},this.variables={},this.getVariableValue=function(e,t){return this.variables.hasOwnProperty(t)?this.variables[t](e,t)||"":this.$getDefaultValue(e,t)||""},this.tmStrFormat=function(e,t,n){var r=t.flag||"",i=t.guard;i=new RegExp(i,r.replace(/[^gi]/,""));var s=this.tokenizeTmSnippet(t.fmt,"formatString"),o=this,u=e.replace(i,function(){o.variables.__=arguments;var e=o.resolveVariables(s,n),t="E";for(var r=0;r=0&&s.splice(o,1)}}var n=this.snippetMap,r=this.snippetNameMap;e.content?i(e):Array.isArray(e)&&e.forEach(i)},this.parseSnippetFile=function(e){e=e.replace(/\r/g,"");var t=[],n={},r=/^#.*|^({[\s\S]*})\s*$|^(\S+) (.*)$|^((?:\n*\t.*)+)/gm,i;while(i=r.exec(e)){if(i[1])try{n=JSON.parse(i[1]),t.push(n)}catch(s){}if(i[4])n.content=i[4].replace(/^\t/gm,""),t.push(n),n={};else{var o=i[2],u=i[3];if(o=="regex"){var a=/\/((?:[^\/\\]|\\.)*)|$/g;n.guard=a.exec(u)[1],n.trigger=a.exec(u)[1],n.endTrigger=a.exec(u)[1],n.endGuard=a.exec(u)[1]}else o=="snippet"?(n.tabTrigger=u.match(/^\S*/)[0],n.name||(n.name=u)):n[o]=u}}return t},this.getSnippetByName=function(e,t){var n=this.snippetNameMap,r;return this.getActiveScopes(t).some(function(t){var i=n[t];return i&&(r=i[e]),!!r},this),r}}).call(a.prototype);var f=function(e){if(e.tabstopManager)return e.tabstopManager;e.tabstopManager=this,this.$onChange=this.onChange.bind(this),this.$onChangeSelection=r.delayedCall(this.onChangeSelection.bind(this)).schedule,this.$onChangeSession=this.onChangeSession.bind(this),this.$onAfterExec=this.onAfterExec.bind(this),this.attach(e)};(function(){this.attach=function(e){this.index=-1,this.ranges=[],this.tabstops=[],this.selectedTabstop=null,this.editor=e,this.editor.on("change",this.$onChange),this.editor.on("changeSelection",this.$onChangeSelection),this.editor.on("changeSession",this.$onChangeSession),this.editor.commands.on("afterExec",this.$onAfterExec),this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler)},this.detach=function(){this.tabstops.forEach(this.removeTabstopMarkers,this),this.ranges=null,this.tabstops=null,this.selectedTabstop=null,this.editor.removeListener("change",this.$onChange),this.editor.removeListener("changeSelection",this.$onChangeSelection),this.editor.removeListener("changeSession",this.$onChangeSession),this.editor.commands.removeListener("afterExec",this.$onAfterExec),this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler),this.editor.tabstopManager=null,this.editor=null},this.onChange=function(e){var t=e.data.range,n=e.data.action[0]=="r",r=t.start,i=t.end,s=r.row,o=i.row,a=o-s,f=i.column-r.column;n&&(a=-a,f=-f);if(!this.$inChange&&n){var l=this.selectedTabstop,c=!l.some(function(e){return u(e.start,r)<=0&&u(e.end,i)>=0});if(c)return this.detach()}var h=this.ranges;for(var p=0;p0){this.removeRange(d),p--;continue}d.start.row==s&&d.start.column>r.column&&(d.start.column+=f),d.end.row==s&&d.end.column>=r.column&&(d.end.column+=f),d.start.row>=s&&(d.start.row+=a),d.end.row>=s&&(d.end.row+=a),u(d.start,d.end)>0&&this.removeRange(d)}h.length||this.detach()},this.updateLinkedFields=function(){var e=this.selectedTabstop;if(!e.hasLinkedRanges)return;this.$inChange=!0;var n=this.editor.session,r=n.getTextRange(e.firstNonLinked);for(var i=e.length;i--;){var s=e[i];if(!s.linked)continue;var o=t.snippetManager.tmStrFormat(r,s.original);n.replace(s,o)}this.$inChange=!1},this.onAfterExec=function(e){e.command&&!e.command.readOnly&&this.updateLinkedFields()},this.onChangeSelection=function(){if(!this.editor)return;var e=this.editor.selection.lead,t=this.editor.selection.anchor,n=this.editor.selection.isEmpty();for(var r=this.ranges.length;r--;){if(this.ranges[r].linked)continue;var i=this.ranges[r].contains(e.row,e.column),s=n||this.ranges[r].contains(t.row,t.column);if(i&&s)return}this.detach()},this.onChangeSession=function(){this.detach()},this.tabNext=function(e){var t=this.tabstops.length-1,n=this.index+(e||1);n=Math.min(Math.max(n,0),t),this.selectTabstop(n),n==t&&this.detach()},this.selectTabstop=function(e){var t=this.tabstops[this.index];t&&this.addTabstopMarkers(t),this.index=e,t=this.tabstops[this.index];if(!t||!t.length)return;this.selectedTabstop=t;if(!this.editor.inVirtualSelectionMode){var n=this.editor.multiSelect;n.toSingleRange(t.firstNonLinked.clone());for(var r=t.length;r--;){if(t.hasLinkedRanges&&t[r].linked)continue;n.addRange(t[r].clone(),!0)}}else this.editor.selection.setRange(t.firstNonLinked);this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler)},this.addTabstops=function(e,t,n){if(!e[0]){var r=i.fromPoints(n,n);c(r.start,t),c(r.end,t),e[0]=[r],e[0].index=0}var s=this.index,o=[s,0],u=this.ranges,a=this.editor;e.forEach(function(e){for(var n=e.length;n--;){var r=e[n],s=i.fromPoints(r.start,r.end||r.start);l(s.start,t),l(s.end,t),s.original=r,s.tabstop=e,u.push(s),e[n]=s,r.fmtString?(s.linked=!0,e.hasLinkedRanges=!0):e.firstNonLinked||(e.firstNonLinked=s)}e.firstNonLinked||(e.hasLinkedRanges=!1),o.push(e),this.addTabstopMarkers(e)},this),o.push(o.splice(2,1)[0]),this.tabstops.splice.apply(this.tabstops,o)},this.addTabstopMarkers=function(e){var t=this.editor.session;e.forEach(function(e){e.markerId||(e.markerId=t.addMarker(e,"ace_snippet-marker","text"))})},this.removeTabstopMarkers=function(e){var t=this.editor.session;e.forEach(function(e){t.removeMarker(e.markerId),e.markerId=null})},this.removeRange=function(e){var t=e.tabstop.indexOf(e);e.tabstop.splice(t,1),t=this.ranges.indexOf(e),this.ranges.splice(t,1),this.editor.session.removeMarker(e.markerId)},this.keyboardHandler=new s,this.keyboardHandler.bindKeys({Tab:function(e){if(t.snippetManager&&t.snippetManager.expandWithTab(e))return;e.tabstopManager.tabNext(1)},"Shift-Tab":function(e){e.tabstopManager.tabNext(-1)},Esc:function(e){e.tabstopManager.detach()},Return:function(e){return!1}})}).call(f.prototype);var l=function(e,t){e.row==0&&(e.column+=t.column),e.row+=t.row},c=function(e,t){e.row==t.row&&(e.column-=t.column),e.row-=t.row};e("./lib/dom").importCssString(".ace_snippet-marker { -moz-box-sizing: border-box; box-sizing: border-box; background: rgba(194, 193, 208, 0.09); border: 1px dotted rgba(211, 208, 235, 0.62); position: absolute;}"),t.snippetManager=new a}),define("ace/autocomplete",["require","exports","module","ace/keyboard/hash_handler","ace/autocomplete/popup","ace/autocomplete/util","ace/lib/event","ace/lib/lang","ace/snippets"],function(e,t,n){var r=e("./keyboard/hash_handler").HashHandler,i=e("./autocomplete/popup").AcePopup,s=e("./autocomplete/util"),o=e("./lib/event"),u=e("./lib/lang"),a=e("./snippets").snippetManager,f=function(){this.autoInsert=!0,this.keyboardHandler=new r,this.keyboardHandler.bindKeys(this.commands),this.blurListener=this.blurListener.bind(this),this.changeListener=this.changeListener.bind(this),this.mousedownListener=this.mousedownListener.bind(this),this.mousewheelListener=this.mousewheelListener.bind(this),this.changeTimer=u.delayedCall(function(){this.updateCompletions(!0)}.bind(this))};(function(){this.$init=function(){this.popup=new i(document.body||document.documentElement),this.popup.on("click",function(e){this.insertMatch(),e.stop()}.bind(this))},this.openPopup=function(e,t,n){this.popup||this.$init(),this.popup.setData(this.completions.filtered);var r=e.renderer;if(!n){this.popup.setRow(0),this.popup.setFontSize(e.getFontSize());var i=r.layerConfig.lineHeight,s=r.$cursorLayer.getPixelPosition(this.base,!0);s.left-=this.popup.getTextLeftOffset();var o=e.container.getBoundingClientRect();s.top+=o.top-r.layerConfig.offset,s.left+=o.left-e.renderer.scrollLeft,s.left+=r.$gutterLayer.gutterWidth,this.popup.show(s,i)}},this.detach=function(){this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler),this.editor.off("changeSelection",this.changeListener),this.editor.off("blur",this.changeListener),this.editor.off("mousedown",this.mousedownListener),this.editor.off("mousewheel",this.mousewheelListener),this.changeTimer.cancel(),this.popup&&this.popup.hide(),this.activated=!1,this.completions=this.base=null},this.changeListener=function(e){var t=this.editor.selection.lead;(t.row!=this.base.row||t.column=n?-1:t+1;break;case"start":t=0;break;case"end":t=n}this.popup.setRow(t)},this.insertMatch=function(e){e||(e=this.popup.getData(this.popup.getRow()));if(!e)return!1;if(e.completer&&e.completer.insertMatch)e.completer.insertMatch(this.editor);else{if(this.completions.filterText){var t=this.editor.selection.getAllRanges();for(var n=0,r;r=t[n];n++)r.start.column-=this.completions.filterText.length,this.editor.session.remove(r)}e.snippet?a.insertSnippet(this.editor,e.snippet):this.editor.execCommand("insertstring",e.value||e)}this.detach()},this.commands={Up:function(e){e.completer.goTo("up")},Down:function(e){e.completer.goTo("down")},"Ctrl-Up|Ctrl-Home":function(e){e.completer.goTo("start")},"Ctrl-Down|Ctrl-End":function(e){e.completer.goTo("end")},Esc:function(e){e.completer.detach()},Space:function(e){e.completer.detach(),e.insert(" ")},Return:function(e){e.completer.insertMatch()},"Shift-Return":function(e){e.completer.insertMatch(!0)},Tab:function(e){e.completer.insertMatch()},PageUp:function(e){e.completer.popup.gotoPageUp()},PageDown:function(e){e.completer.popup.gotoPageDown()}},this.gatherCompletions=function(e,t){var n=e.getSession(),r=e.getCursorPosition(),i=n.getLine(r.row),o=s.retrievePrecedingIdentifier(i,r.column);this.base=e.getCursorPosition(),this.base.column-=o.length;var u=[];return s.parForEach(e.completers,function(t,i){t.getCompletions(e,n,r,o,function(e,t){e||(u=u.concat(t)),i()})},function(){t(null,{prefix:o,matches:u})}),!0},this.showPopup=function(e){this.editor&&this.detach(),this.activated=!0,this.editor=e,e.completer!=this&&(e.completer&&e.completer.detach(),e.completer=this),e.keyBinding.addKeyboardHandler(this.keyboardHandler),e.on("changeSelection",this.changeListener),e.on("blur",this.blurListener),e.on("mousedown",this.mousedownListener),e.on("mousewheel",this.mousewheelListener),this.updateCompletions()},this.updateCompletions=function(e){if(e&&this.base&&this.completions){var t=this.editor.getCursorPosition(),n=this.editor.session.getTextRange({start:this.base,end:t});if(n==this.completions.filterText)return;this.completions.setFilter(n);if(!this.completions.filtered.length)return this.detach();this.openPopup(this.editor,n,e);return}this.gatherCompletions(this.editor,function(t,n){var r=n&&n.matches;if(!r||!r.length)return this.detach();this.completions=new l(r),this.completions.setFilter(n.prefix);var i=this.completions.filtered;if(!i.length)return this.detach();if(this.autoInsert&&i.length==1)return this.insertMatch(i[0]);this.openPopup(this.editor,n.prefix,e)}.bind(this))},this.cancelContextMenu=function(){var e=function(t){this.editor.off("nativecontextmenu",e),t&&t.domEvent&&o.stopEvent(t.domEvent)}.bind(this);setTimeout(e,10),this.editor.on("nativecontextmenu",e)}}).call(f.prototype),f.startCommand={name:"startAutocomplete",exec:function(e){e.completer||(e.completer=new f),e.completer.showPopup(e),e.completer.cancelContextMenu()},bindKey:"Ctrl-Space|Ctrl-Shift-Space|Alt-Space"};var l=function(e,t,n){this.all=e,this.filtered=e,this.filterText=t||""};(function(){this.setFilter=function(e){if(e.length>this.filterText&&e.lastIndexOf(this.filterText,0)===0)var t=this.filtered;else var t=this.all;this.filterText=e,t=this.filterCompletions(t,this.filterText),t=t.sort(function(e,t){return t.exactMatch-e.exactMatch||t.score-e.score});var n=null;t=t.filter(function(e){var t=e.value||e.caption||e.snippet;return t===n?!1:(n=t,!0)}),this.filtered=t},this.filterCompletions=function(e,t){var n=[],r=t.toUpperCase(),i=t.toLowerCase();e:for(var s=0,o;o=e[s];s++){var u=o.value||o.caption||o.snippet;if(!u)continue;var a=-1,f=0,l=0,c,h;for(var p=0;p=0?v<0||d0&&(a===-1&&(l+=10),l+=h),f|=1<o-t&&!r?(s.style.top="",s.style.bottom=o-l+"px",n.isTopdown=!1):(l+=t,s.style.top=l+"px",s.style.bottom="",n.isTopdown=!0),s.style.display="",this.renderer.$textLayer.checkForSizeChanges();var c=e.left;c+s.offsetWidth>u&&(c=u-s.offsetWidth),s.style.left=c+"px",this._signal("show"),i=null,n.isOpen=!0},n.getTextLeftOffset=function(){return this.$borderSize+this.renderer.$padding+this.$imageSize},n.$imageSize=0,n.$borderSize=1,n};f.importCssString(".ace_autocomplete.ace-tm .ace_marker-layer .ace_active-line { background-color: #CAD6FA; z-index: 1;}.ace_autocomplete.ace-tm .ace_line-hover { border: 1px solid #abbffe; margin-top: -1px; background: rgba(233,233,253,0.4);}.ace_autocomplete .ace_line-hover { position: absolute; z-index: 2;}.ace_rightAlignedText { color: gray; display: inline-block; position: absolute; right: 4px; text-align: right; z-index: -1;}.ace_autocomplete .ace_completion-highlight{ color: #000; text-shadow: 0 0 0.01em;}.ace_autocomplete { width: 280px; z-index: 200000; background: #fbfbfb; color: #444; border: 1px lightgray solid; position: fixed; box-shadow: 2px 3px 5px rgba(0,0,0,.2); line-height: 1.4;}"),t.AcePopup=c}),define("ace/autocomplete/util",["require","exports","module"],function(e,t,n){t.parForEach=function(e,t,n){var r=0,i=e.length;i===0&&n();for(var s=0;s=0;s--){if(!n.test(e[s]))break;i.push(e[s])}return i.reverse().join("")},t.retrieveFollowingIdentifier=function(e,t,n){n=n||r;var i=[];for(var s=t;s"),u||l.push(""),f.$renderLine(l,h,!0,!1),l.push("\n");var p="
"+"
"+l.join("")+"
"+"
";return f.destroy(),{css:s+n.cssText,html:p,session:a}},n.exports=a,n.exports.highlight=a});(function() {window.require(["ace/ext/static_highlight"], function() {});})(); ================================================ FILE: client/app/js/anal.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(function(){ window.dataLayer = window.dataLayer || []; var dataLayer; function gtag(){ dataLayer.push(arguments); } // Set up Analytics if (window.oo_configGtag) { var cache = []; dataLayer = cache; window.oo_configGtag(function(config) { dataLayer = window.dataLayer; gtag("js", new Date()); gtag("config", "{!gtagid!}", config); Array.prototype.push.apply(dataLayer, cache); }); } else { dataLayer = window.dataLayer; gtag("js", new Date()); gtag("config", "{!gtagid!}"); } function sendEvent(event_category, event_action, event_label, value) { // analytics.js: // garef("send", "event", event_category, event_action, event_label, value); // gtag.js: gtag("event", event_action, { event_category: event_category, event_label: event_label, value: value, }); } // Wait to load Google Analytics to not slow down the module require(["js/runtime"], function(){ require(["gtag"], function(){ // Record browser window size var width = window.innerWidth || document.body.clientWidth; var height = window.innerHeight || document.body.clientHeight; width = Math.round(width/50)*50; height = Math.round(height/50)*50; sendEvent("browser-size", "width", width); sendEvent("browser-size", "height", height); sendEvent("browser-size", "combined", width+"x"+height); }); }); var numExtraTime = 0; // Return methods to register certain events return { pageview: function(){ // Obsolete in gtag.js // _ga("send", "pageview"); }, signedin: function(){ sendEvent("accounts", "signed-in"); }, welcomeback: function() { sendEvent("accounts", "welcome-back"); }, sitecontrol: function(which){ sendEvent("site-control", which); }, command: function(cmd){ sendEvent("command", "user-cmd", cmd.substr(0,5), cmd.length); }, runfile: function(){ sendEvent("command", "run-file"); }, sigint: function(){ sendEvent("signal", "user-interrupt"); }, patience: function(){ sendEvent("loading", "patience-message"); }, dismiss: function(what){ sendEvent("dismiss", "promo", what); }, duration: function(duration){ sendEvent("command", "duration", "millis", duration); }, extraTime: function(){ sendEvent("extra-time", "from-prompt", numExtraTime++); }, acknowledgePayload: function(){ sendEvent("extra-time", "acknowledge-payload"); }, alert: function(message){ sendEvent("alert", "alert", message.substr(0,20), message.length); } }; }); ================================================ FILE: client/app/js/app.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define( ["knockout", "socket.io", "js/client", "ace/ace", "jquery", "ismobile", "splittr", "SocketIOFileUpload", "js/anal", "js/onboarding", "js/ot-handler", "js/ws-shared", "js/utils", "ko-flash", "ace/mode/octave", "ace/ext/language_tools", "js/ko-ace", "js/flex-resize"], function (ko, io, OctMethods, ace, $, isMobile, splittr, SocketIOFileUpload, anal, onboarding, OtHandler, WsShared) { // Initial GUI setup splittr.init(); // Make conveinence variable references var viewModel = OctMethods.ko.viewModel; var allOctFiles = OctMethods.ko.allOctFiles; var vars = viewModel.vars; // Run Knockout ko.applyBindings(viewModel); // Make Socket Connection and Add Listeners: // The first two lines are a hack to make injected configs work var ioPath = "{!socket_io_path!}"; var socket = (ioPath[0] === "{") ? io() : io({ path: ioPath }); OctMethods.socketListeners.subscribe(socket); OtHandler.listeners.subscribe(socket); WsShared.listeners.subscribe(socket); OctMethods.socket.instance = socket; // Autocompletion with filenames: ace.require("ace/ext/language_tools").addCompleter({ getCompletions: function (editor, session, pos, prefix, callback) { callback(null, $.map(allOctFiles(), function (file) { var filenameNoExtension = file.filename().replace(/\.[^/.]+$/, ""); return { value: filenameNoExtension, meta: "file" }; })); } }); // Autocompletion with variables: ace.require("ace/ext/language_tools").addCompleter({ getCompletions: function (editor, session, pos, prefix, callback) { callback(null, ko.utils.arrayMap(vars(), function(v){ return { value: v.symbol(), meta: "var" }; })); } }); // Make Prompt: var prompt = ace.edit("prompt"); prompt.setTheme(viewModel.selectedSkin().aceTheme); prompt.getSession().setMode("ace/mode/octave"); prompt.renderer.setShowGutter(false); prompt.setHighlightActiveLine(false); prompt.setShowPrintMargin(false); prompt.setOptions({ enableBasicAutocompletion: true, maxLines: 6 }); prompt.setBehavioursEnabled(false); // disables quto-quote prompt.renderer.setScrollMargin(5, 5); prompt.getSession().setUseWrapMode(true); prompt.commands.addCommand({ name: "nullifyLineNumber", bindKey: {mac: "Command-L", win: "Ctrl-L"}, exec: function () { }, readOnly: true }); prompt.commands.addCommand({ name: "previousCommand", bindKey: "Up", exec: OctMethods.promptListeners.historyUp, readOnly: false }); prompt.commands.addCommand({ name: "nextCommand", bindKey: "Down", exec: OctMethods.promptListeners.historyDown, readOnly: false }); prompt.commands.addCommand({ name: "startAutocompleteOnTab", bindKey: "Tab", exec: ace.require("ace/autocomplete").Autocomplete.startCommand.exec, readOnly: false }); prompt.commands.addCommand({ name: "submitPrompt", bindKey: {win: "Enter", mac: "Enter"}, exec: OctMethods.promptListeners.command, readOnly: false }); OctMethods.prompt.instance = prompt; // Initialize the console screen OctMethods.console.clear(); // Add Prompt/Console/Plot Listeners: $("#signal").click(OctMethods.promptListeners.signal); $("#console").on("click", ".prompt_command", OctMethods.promptListeners.permalink); // Add listeners to the file list toolbar $("#files_toolbar_create").click(OctMethods.editorListeners.newCB); $("#files_toolbar_refresh").click(OctMethods.editorListeners.refresh); $("#files_toolbar_info").click(OctMethods.editorListeners.info); // Set up the file uploader: try { var siofu = new SocketIOFileUpload(socket); siofu.useBuffer = false; var _addDragClass = function () { $(this).addClass("drag-over"); }; var _removeDragClass = function () { $(this).removeClass("drag-over"); }; $("#files_list_container").on("dragover", _addDragClass); $("#files_list_container").on("dragenter", _addDragClass); $("#files_list_container").on("dragleave", _removeDragClass); $("#files_list_container").on("drop", _removeDragClass); siofu.listenOnDrop($("#files_list_container")[0]); $("#files_toolbar_upload").click(siofu.prompt); } catch (e) { // SIOFU not supported in current browser console.error(e); } var currentUrl = new URL(window.location.href); // Shared workspace setup var wsId = currentUrl.searchParams.get("w"); if (wsId) { OctMethods.vars.wsId = wsId; viewModel.purpose("shared"); viewModel.selectedSkin(OctMethods.ko.availableSkins[2]); } // Student workspace setup var studentId = currentUrl.searchParams.get("s"); var match; if (!studentId) { match = currentUrl.pathname.match(/^\/workspace~(\w+)$/); if (match) studentId = match[1]; } if (studentId) { OctMethods.vars.studentId = studentId; viewModel.purpose("student"); viewModel.selectedSkin(OctMethods.ko.availableSkins[2]); } // Bucket setup var bucketId = currentUrl.searchParams.get("b"); if (!bucketId) { match = currentUrl.pathname.match(/^\/bucket~(\w+)$/); if (match) bucketId = match[1]; } if (bucketId) { OctMethods.vars.bucketId = bucketId; viewModel.purpose("bucket"); viewModel.selectedSkin(OctMethods.ko.availableSkins[3]); onboarding.showBucketPromo(); } else { match = currentUrl.pathname.match(/^\/project~(\w+)$/); if (match) { OctMethods.vars.bucketId = match[1]; viewModel.purpose("project"); } } // Global key bindings: $(window).keydown(function (e) { if (e.keyCode == 82) { // "R" key if (e.metaKey || e.ctrlKey) { OctMethods.editorListeners.keyRun(e); } } else if (e.keyCode == 69) { // "E" key if (e.metaKey || e.ctrlKey) { OctMethods.promptListeners.keyFocus(e); } } }); /* * * * END EDITOR/CONSOLE/PROMPT, START GUI * * * */ // Privacy Policy $("#privacy").find("[data-purpose='close']").click(function () { anal.sitecontrol("privacy-close"); }); $("#showprivacy").click(function () { $("#privacy").showSafe(); anal.sitecontrol("privacy"); }); // Mobile GUI if (isMobile) { window.matchMedia("(orientation:portrait)").addListener(function () { OctMethods.console.scroll(); }); } // Sign-In and Password $("#hamburger, #sign_in_shortcut").click(function () { var opened = $("#main_menu").toggleSafe(); $("#hamburger").toggleClass("is-active", opened); onboarding.hideScriptPromo(); onboarding.hideBucketPromo(); anal.sitecontrol("hamburger"); }); $("#sign_in_with_google").click(function () { window.location.href = "/auth/google"; }); $("#sign_in_with_password").click(function () { $("#email_password").showSafe(); $("#emailField2").focus(); }); $("#sign_in_with_email").click(function () { $("#email_token").showSafe(); $("#emailField1").focus(); $("#passwordField1").hideSafe(); // !! HONEYPOT !! }); // Callback for #create-password-btn is in Knockout setup $("#save-password-btn").click(function() { var password = $("#new_pwd").val(); $("#new_pwd").val(""); $("#change_password").hideSafe(); OctMethods.socket.setPassword(password); }); // Theme bindings function updateTheme(newValue) { $("#theme").attr("href", newValue.cssURL); $("body").attr("data-sanscons-color", newValue.iconColor); $("[data-theme]").hideSafe(); $("[data-theme='"+newValue.name+"']").showSafe(); OctMethods.prompt.instance.setTheme(newValue.aceTheme); } updateTheme(viewModel.selectedSkin()); viewModel.selectedSkin.subscribe(updateTheme); function toggleTheme(dark) { var newSkin; if (dark) { newSkin = OctMethods.ko.availableSkins[1]; } else switch(viewModel.purpose()) { case "student": newSkin = OctMethods.ko.availableSkins[2]; break; case "bucket": newSkin = OctMethods.ko.availableSkins[3]; break; case "project": if (viewModel.currentBucket() && viewModel.currentBucket().butype() === "collab") { newSkin = OctMethods.ko.availableSkins[2]; } else { newSkin = OctMethods.ko.availableSkins[3]; } break; case "default": default: newSkin = OctMethods.ko.availableSkins[0]; break; } viewModel.selectedSkin(newSkin); viewModel.prefersDarkMode(dark); OctMethods.prompt.focus(); anal.sitecontrol("theme"); } $("#change-skin").click(function () { toggleTheme(!viewModel.prefersDarkMode()); }); window.matchMedia("(prefers-color-scheme: dark)").addListener(function(updated) { toggleTheme(updated.matches); }); viewModel.currentBucket.subscribe(function() { toggleTheme(viewModel.prefersDarkMode()); }); // Callouts positioned relative to non-top-level elements window.addEventListener("resize", function() { onboarding.reposition(); }, false); // Other GUI Initialization OctMethods.prompt.disable(); $("#twitter-follow-holder").click(function () { anal.sitecontrol("twitter"); }); $("#feedback-btn").click(function () { anal.sitecontrol("feedback"); }); $("#reset-layout").click(function () { viewModel.flex.sizes([100, 400, 75, 325]); viewModel.flex.shown(true); }); $("[data-purpose='close']").click(function () { // Clicking on a close button in a popover box $(this).closest("[data-purpose='popover']").fadeOutSafe(250); OctMethods.prompt.focus(); }); OctMethods.load.startPatience(); /* * * * END GUI * * * */ } ); // AMD Define ================================================ FILE: client/app/js/base64-toBlob.js ================================================ /* eslint-disable */ /* * Copyright © 2013 Jeremy (@jeremy.ca) * * This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 * Unported License. To view a copy of this license, visit * http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative * Commons, PO Box 1866, Mountain View, CA 94042, USA. * * This code is from Stack Overflow, retrieved from * . * * More details: . */ define("base64-toblob", ["base64", "blob"], function(Base64){ return function(b64Data, contentType, sliceSize) { contentType = contentType || ''; sliceSize = sliceSize || 512; var byteCharacters = atob(b64Data, true); var byteArrays = []; for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { var slice = byteCharacters.slice(offset, offset + sliceSize); var byteNumbers = new Array(slice.length); for (var i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } var byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } return new Blob(byteArrays, {type: contentType}); }; }); ================================================ FILE: client/app/js/base64v1.module.js ================================================ /* eslint-disable */ /* Copyright Vassilis Petroulias [DRDigit] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Web Site - http://jsbase64.codeplex.com/ */ define("base64", function(){ var B64 = { alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', lookup: null, ie: /MSIE /.test(navigator.userAgent), ieo: /MSIE [67]/.test(navigator.userAgent), encode: function (s) { var buffer = B64.toUtf8(s), position = -1, len = buffer.length, nan0, nan1, nan2, enc = [, , , ]; if (B64.ie) { var result = []; while (++position < len) { nan0 = buffer[position]; nan1 = buffer[++position]; enc[0] = nan0 >> 2; enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4); if (isNaN(nan1)) enc[2] = enc[3] = 64; else { nan2 = buffer[++position]; enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6); enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63; } result.push(B64.alphabet.charAt(enc[0]), B64.alphabet.charAt(enc[1]), B64.alphabet.charAt(enc[2]), B64.alphabet.charAt(enc[3])); } return result.join(''); } else { var result = ''; while (++position < len) { nan0 = buffer[position]; nan1 = buffer[++position]; enc[0] = nan0 >> 2; enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4); if (isNaN(nan1)) enc[2] = enc[3] = 64; else { nan2 = buffer[++position]; enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6); enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63; } result += B64.alphabet[enc[0]] + B64.alphabet[enc[1]] + B64.alphabet[enc[2]] + B64.alphabet[enc[3]]; } return result; } }, decode: function (s) { if (s.length % 4) throw new Error("InvalidCharacterError: 'B64.decode' failed: The string to be decoded is not correctly encoded."); var buffer = B64.fromUtf8(s), position = 0, len = buffer.length; if (B64.ieo) { var result = []; while (position < len) { if (buffer[position] < 128) result.push(String.fromCharCode(buffer[position++])); else if (buffer[position] > 191 && buffer[position] < 224) result.push(String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63))); else result.push(String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63))); } return result.join(''); } else { var result = ''; while (position < len) { if (buffer[position] < 128) result += String.fromCharCode(buffer[position++]); else if (buffer[position] > 191 && buffer[position] < 224) result += String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63)); else result += String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63)); } return result; } }, toUtf8: function (s) { var position = -1, len = s.length, chr, buffer = []; if (/^[\x00-\x7f]*$/.test(s)) while (++position < len) buffer.push(s.charCodeAt(position)); else while (++position < len) { chr = s.charCodeAt(position); if (chr < 128) buffer.push(chr); else if (chr < 2048) buffer.push((chr >> 6) | 192, (chr & 63) | 128); else buffer.push((chr >> 12) | 224, ((chr >> 6) & 63) | 128, (chr & 63) | 128); } return buffer; }, fromUtf8: function (s) { var position = -1, len, buffer = [], enc = [, , , ]; if (!B64.lookup) { len = B64.alphabet.length; B64.lookup = {}; while (++position < len) B64.lookup[B64.alphabet.charAt(position)] = position; position = -1; } len = s.length; while (++position < len) { enc[0] = B64.lookup[s.charAt(position)]; enc[1] = B64.lookup[s.charAt(++position)]; buffer.push((enc[0] << 2) | (enc[1] >> 4)); enc[2] = B64.lookup[s.charAt(++position)]; if (enc[2] == 64) break; buffer.push(((enc[1] & 15) << 4) | (enc[2] >> 2)); enc[3] = B64.lookup[s.charAt(++position)]; if (enc[3] == 64) break; buffer.push(((enc[2] & 3) << 6) | enc[3]); } return buffer; } }; // Also expose as window.btoa and window.atob if (!window.btoa) window.btoa = B64.encode; if (!window.atob) window.atob = B64.decode; return B64; }); ================================================ FILE: client/app/js/bucket.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(["knockout", "require", "js/octfile", "js/utils"], function(ko, require, OctFile, utils){ var OctMethods = require("js/client"); // Bucket MVVM class function Bucket(){ // the "self" variable enables us to refer to the Bucket context even when // we are programming within callback function contexts var self = this; // Main Bindings self.files = ko.observableArray(); self.main = ko.observable(null); self.id = ko.observable(null); self.createdTime = ko.observable(new Date()); self.mainFilename = ko.pureComputed({ read: function() { return self.main() ? self.main().filename() : null; }, write: function(filename) { if (filename) { self.main(new OctFile(filename, "", false)); } else { self.main(null); } } }); self.butype = ko.observable("readonly"); self.base_bucket_id = ko.observable(null); self.baseModel = ko.observable(null); self.shortlink = ko.observable(null); self.url = ko.computed(function() { var prefix = (self.butype() === "readonly") ? "bucket" : "project"; return window.location.origin + "/" + prefix + "~" + self.id(); }); self.shortUrl = ko.computed(function() { return oo_translations["constants.shortlink_prefix"] + self.shortlink(); }); self.createdTimeString = ko.computed(function() { return self.createdTime().toLocaleString(); }); self.isOwnedByCurrentUser = ko.computed(function() { return !!ko.utils.arrayFirst(OctMethods.ko.viewModel.allBuckets(), function(bucket) { return bucket.id() === self.id(); }, null); }); // Bindings used during bucket creation self.selectedLeft = ko.observableArray(); self.selectedRight = ko.observableArray(); self.showCreateButton = ko.observable(true); self.filesNotIncluded = ko.pureComputed(function() { return utils.sortedFilter(OctMethods.ko.allOctFiles(), self.files(), function(octfile) { return octfile.filename(); }); }); self.moveLeftToRight = function() { self.files.push.apply(self.files, self.selectedLeft()); self.selectedLeft.removeAll(); self.files.sort(OctFile.sorter); }; self.moveRightToLeft = function() { self.files.removeAll(self.selectedRight()); self.selectedRight.removeAll(); }; self.textFiles = ko.pureComputed(function() { return ko.utils.arrayFilter(self.files(), function(octfile) { return octfile.editable; }); }); self.setAutoShortlink = function() { var shortlink = ""; // 5 lowercase letters for (var i = 0; i < 5; i++) { shortlink += String.fromCharCode(97 + Math.floor(Math.random() * 26)); } // 3 numbers for (var i = 0; i < 3; i++) { shortlink += Math.floor(Math.random() * 10); } self.shortlink(shortlink); }; self.editShortlink = function() { var result = window.prompt(oo_translations["buckets.label1"] + "\n\n" + oo_translations["constants.shortlink_prefix"] + "…", self.shortlink()); if (result) { OctMethods.socket.changeBucketShortlink(self, result); } } self.createOnServer = function() { OctMethods.socket.createBucket(self); self.showCreateButton(false); }; self.deleteit = function() { if (confirm("Are you sure you want to delete this bucket?\n\n" + self.shortUrl())) { OctMethods.socket.deleteBucket(self); } }; } Bucket.fromBucketInfo = function(info) { var bucket = new Bucket(); bucket.id(info.bucket_id); bucket.mainFilename(info.main); bucket.createdTime(new Date(info.createdTime)); bucket.butype(info.butype); bucket.base_bucket_id(info.base_bucket_id); bucket.shortlink(info.shortlink); if (info.baseModel) { bucket.baseModel(Bucket.fromBucketInfo(info.baseModel)); } return bucket; }; // Expose interface return Bucket; }); ================================================ FILE: client/app/js/client.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Client-Side JavaScript for Octave Online define(["jquery", "knockout", "canvg", "base64", "js/download", "ace/ext/static_highlight", "js/anal", "base64-toblob", "ismobile", "exports", "js/octfile", "js/bucket", "js/vars", "ko-takeArray", "require", "js/onboarding", "js/ws-shared", "js/utils", "blob", "jquery.md5", "ace/theme/crimson_editor", "ace/theme/merbivore_soft", "js/ko-ace"], function($, ko, canvg, Base64, download, aceStaticHighlight, anal, b64ToBlob, isMobile, exports, OctFile, Bucket, Var, koTakeArray, require, onboarding, WsShared, utils){ if (!window.oo_translations) { console.error("WARNING: Translations not found. UI text will be unavailable."); } var oo_translations = window.oo_translations || {}; var oo_currentLanguage = window.oo_currentLanguage || "und"; var oo_availableLanguages = window.oo_availableLanguages || ["und"]; /* * * * START KNOCKOUT SETUP * * * */ // Skin MVVM class function Skin(name, aceTheme, iconColor){ var self = this; // Main Bindings self.name = name; self.displayName = name.charAt(0).toUpperCase() + name.slice(1); self.iconColor = iconColor; self.rawAceTheme = aceTheme; self.aceTheme = "ace/theme/"+aceTheme; self.cssURL = "css/themes/"+self.name+".css?{!css-timestamp!}"; } var availableSkins = [ new Skin("fire", "crimson_editor", "black"), new Skin("lava", "merbivore_soft", "white"), new Skin("ice", "crimson_editor", "black"), new Skin("sun", "crimson_editor", "black"), ]; // Initialization for skin and dark mode. // April 2019: This has to be done this way for backwards compatibility. Eventually, oldSelectedSkin can be deleted. var oldSelectedSkin = ko.observable(); oldSelectedSkin.extend({ localStorage: "selected-skin" }); var prefersDarkMode = ko.observable(false); if (oldSelectedSkin() && oldSelectedSkin().name === "lava") { prefersDarkMode(true); } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { prefersDarkMode(true); } prefersDarkMode.extend({ localStorage: "prefers-dark-mode" }); var defaultSkin; if (prefersDarkMode()) { defaultSkin = availableSkins[1]; } else { defaultSkin = availableSkins[0]; } // Plot MVVM class function PlotObject(id){ var self = this; // Main Bindings self.id = id; self.lineNumber = ko.observable(null); self.data = ""; // not an observable for performance reasons self.complete = ko.observable(false); // Functions self.addData = function(data){ self.data += data; }; self.setCurrent = function(){ var arr = plotHistory(); for (var i = arr.length - 1; i >= 0; i--) { if (arr[i].id === id) { currentPlotIdx(i); } } }; self.downloadPng = function(){ var plotCanvas = document.getElementById("plot_canvas"); var filename = "octave-online-line-" + self.lineNumber() + ".png"; var renderCallback = function(){ plotCanvas.toBlob(function(blob){ download(blob, filename); }, "image/png"); }; canvg(plotCanvas, self.data, { renderCallback: renderCallback, ignoreMouse: true }); }; self.downloadSvg = function(){ var blob = new Blob([self.data], { type: "image/svg+xml" }); var filename = "octave-online-line-" + self.lineNumber() + ".svg"; download(blob, filename); }; self.completeData = ko.computed(function(){ if (self.complete()) { return self.data; } else { return ""; } }); self.md5 = ko.computed(function(){ return $.md5(self.completeData()); }); self.bindElement = function($el){ self.complete.subscribe(function(){ $el.append(self.completeData()); $el.find(".inline-plot-loading").fadeOut(500); }); }; } // Initialize MVVM variables var allOctFiles = ko.observableArray([]); var selectedSkin = ko.observable(defaultSkin); var purpose = ko.observable("default"); var vars = ko.observableArray([]); var plotHistory = ko.observableArray([]); var currentPlotIdx = ko.observable(-1); var authUser = ko.observable(); // user who is currently logged in var currentUser = ko.observable(); // user who owns the workspace (may or may not be the same as authUser) var currentBucket = ko.observable(); var viewModel = window.viewModel = { files: allOctFiles, openFile: ko.observable(), close: function(){ OctMethods.editor.close(); }, selectedSkin: selectedSkin, prefersDarkMode: prefersDarkMode, purpose: purpose, vars: vars, plots: plotHistory, currentPlotIdx: currentPlotIdx, inlinePlots: ko.observable(true), consoleWhiteSpaceWrap: ko.observable(true), instructorPrograms: ko.observableArray(), allBuckets: ko.observableArray(), newBucket: ko.observable(), countdownExtraTimeSeconds: ko.observable(), currentLanguage: ko.observable(oo_currentLanguage), availableLanguages: ko.observableArray(oo_availableLanguages), // More for UI logoSrc: ko.computed(function() { var color = selectedSkin().iconColor; if (purpose() === "bucket") { return "images/logos/banner-" + color + "-bucket.svg"; } else { return "images/logos/banner-" + color + ".svg"; } }), patreonValue: ko.computed(function() { var user = authUser(); return user && user.patreon && user.patreon.currently_entitled_amount_cents; }), openUserVoice: function() { require(["uservoice"], function() { window.UserVoice.push(["showLightbox", "classic_widget", { mode: "full", primary_color: "#cc6d00", link_color: "#007dbf", default_mode: "support", forum_id: 211888, }]); }); }, /* openUserVoiceSupport: function() { require(["uservoice"], function() { window.UserVoice.push(["showLightbox", "classic_widget", { mode: "support", primary_color: "#cc6d00", link_color: "#007dbf" }]); }); }, */ // More for plots currentPlot: ko.computed(function(){ if (currentPlotIdx()<0) return null; return plotHistory()[currentPlotIdx()]; }), showPlot: ko.computed(function(){ return currentPlotIdx() >= 0; }), togglePlot: function(){ var idx = currentPlotIdx(); var len = plotHistory().length; if (len === 0) { utils.alert(oo_translations["console.plotwindow#alert"]); } else if (idx < 0) { currentPlotIdx(len-1); } else { currentPlotIdx(-1); } OctMethods.prompt.focus(); }, firstPlotShown: ko.computed(function(){ return currentPlotIdx() === 0; }), lastPlotShown: ko.computed(function(){ return currentPlotIdx()+1 === plotHistory().length; }), showPrevPlot: function(){ var idx = currentPlotIdx(); if (idx <= 0) return null; currentPlotIdx(idx - 1); }, showNextPlot: function(){ var idx = currentPlotIdx(); var len = plotHistory().length; if (idx+1 >= len) return null; currentPlotIdx(idx + 1); }, plotZoomed: ko.observable(false), zoomPlot: function(){ viewModel.plotZoomed(!viewModel.plotZoomed()); }, // Sign In / Sign Out authUser: authUser, currentUser: currentUser, doLogout: function(){ onboarding.reset(); window.location.href = "/logout"; }, showChangePassword: function() { anal.sitecontrol("changepwdbtn"); $("#change_password").showSafe(); $("#new_pwd").focus(); }, unenrollStudent: function(user) { if (confirm(oo_translations["students.unenroll.p1"] + "\n\n" + oo_translations["students.name#label"] + " " + user.displayName + "\n" + oo_translations["students.course#label"] + " " + user.program)) { OctMethods.socket.unenrollStudent(user); } }, reenrollStudent: function(user) { var newProgram = prompt(oo_translations["students.reenroll.p1"], ""); var programs = viewModel.instructorPrograms(); for (var i=0; i= 0; i--) { if (arr[i].id === id) return arr[i]; } // Make a new plot object var obj = new PlotObject(id); plotHistory.push(obj); // Display it, either inline or in the plot window if (viewModel.inlinePlots()) { obj.bindElement(OctMethods.console.writePlot()); } else { obj.setCurrent(); } return obj; } // Define a massive singleton object to contain all methods and listeners var OctMethods = { // Console Methods console: { currentInContentAdTimestamp: 0, write: function(content){ $("#console").append(document.createTextNode(content)); OctMethods.console.scroll(); }, writeError: function(content){ var span = $(""); span.append(document.createTextNode(content)); $("#console").append(span); OctMethods.console.scroll(); }, writeRow: function(rowString){ var rowSpan = $(""); rowSpan.append(document.createTextNode(rowString)); $("#console").append(rowSpan); }, writeCommand: function(lineNumber, cmd){ var rowString; if(lineNumber >= 0){ rowString = "octave:" + lineNumber + "> "; }else if(lineNumber === -1){ rowString = "> "; }else{ rowString = ""; } if(rowString) OctMethods.console.writeRow(rowString); var commandSpan = $(""); commandSpan.append(document.createTextNode(cmd)); $("#console").append(commandSpan); $("#console").append(document.createTextNode("\n")); OctMethods.console.scroll(); }, writeRestartBtn: function(){ var options = $(""); // Construct the normal restart button var btn1 = $(""); btn1.click(function(){ OctMethods.socket.reconnect(); options.remove(); }); btn1.append(document.createTextNode(oo_translations["console.reconnect#btn"])); options.append(btn1); // Append to the console $("#console").append(options); $("#console").append(document.createTextNode("\n")); OctMethods.console.scroll(); }, writeUrl: function(url, linkText){ if (!linkText) linkText = url; var el = $(""); el.attr("href", url); el.attr("target", "_blank"); el.append(document.createTextNode(linkText)); $("#console").append(document.createTextNode(oo_translations["console.seeurl#label"])); $("#console").append(document.createTextNode(" ")); $("#console").append(el); $("#console").append(document.createTextNode("\n")); OctMethods.console.scroll(); }, writePlot: function(){ var el = $("
"); el.attr("class", "inline-plot"); var loading = $("
"); loading.attr("class", "inline-plot-loading"); el.append(loading); $("#console").append(el); OctMethods.console.scroll(); return el; }, scroll: function(){ $("#console").scrollTop($("#console")[0].scrollHeight); $("#type_here").hideSafe(); $("#agpl_icon").hideSafe(); $("#tier_background").hideSafe(); $("#plot_opener").showSafe(); }, clear: function(){ $("#console").empty(); }, command: function(cmd, skipsend){ if(!OctMethods.prompt.enabled) return; var currentLine = OctMethods.prompt.currentLine; // In-content ad opportunity if (window.oo_inConsoleAd && currentLine >= 3) { window.oo_inConsoleAd(); } // Show the command on screen OctMethods.console.writeCommand(currentLine, cmd); // Add command to history var history = OctMethods.prompt.history; if (cmd !== "" && history[history.length-2] !== cmd) { history[history.length-1] = cmd; history.push(""); } OctMethods.prompt.index = history.length - 1; // Start countdown OctMethods.prompt.startCountdown(); OctMethods.prompt.disable(); // Send to server if (!skipsend) { OctMethods.socket.command(cmd); } } }, // Prompt Methods prompt: { instance: null, currentLine: 0, history: [""], index: 0, legalTime: parseInt("5000!config.session.legalTime.guest"), extraTime: 0, countdownExtraTime: parseInt("15000!config.session.countdownExtraTime"), countdownRequestTime: parseInt("3000!config.session.countdownRequestTime"), countdownInterval: null, payloadTimerInterval: null, payloadDelay: -1, countdownTime: 0, countdownDelay: 20, enabled: true, enable: function(){ $("#runtime_controls_container").hideSafe(); $("#prompt").showSafe(); $("#prompt_sign").showSafe(); OctMethods.prompt.enabled = true; OctMethods.prompt.endCountdown(); // There is a bug/feature in ACE that disables rendering when the element is hidden with display: none. This hack forces a re-render now. OctMethods.prompt.instance.resize(true); }, disable: function(){ $("#prompt").hideSafe(); $("#prompt_sign").hideSafe(); OctMethods.prompt.enabled = false; }, clear: function(){ OctMethods.prompt.instance.setValue(""); }, focus: function(){ OctMethods.prompt.instance.focus(); }, startCountdown: function(){ $("#add_time_container").hideSafe(); $("#payload_acknowledge_container").hideSafe(); $("#runtime_controls_container").showSafe(); OctMethods.prompt.countdownTime = new Date().valueOf(); OctMethods.prompt.extraTime = 0; OctMethods.prompt.countdownTick(); clearInterval(OctMethods.prompt.countdownInterval); OctMethods.prompt.countdownInterval = setInterval( OctMethods.prompt.countdownTick, OctMethods.prompt.countdownDelay ); }, countdownTick: function(){ var elapsed = new Date().valueOf() - OctMethods.prompt.countdownTime; var remaining = (OctMethods.prompt.legalTime + OctMethods.prompt.extraTime - elapsed); if(remaining<=0) { clearInterval(OctMethods.prompt.countdownInterval); $("#seconds_remaining").text("---"); }else{ $("#seconds_remaining").text((remaining/1000).toFixed(2)); } if (remaining <= OctMethods.prompt.countdownRequestTime) { $("#add_time_container").showSafe(); } else { $("#add_time_container").hideSafe(); } }, endCountdown: function(){ clearInterval(OctMethods.prompt.countdownInterval); clearInterval(OctMethods.prompt.payloadTimerInterval); $("#runtime_controls_container").hideSafe(); $("#seconds_remaining").text("0"); if (OctMethods.prompt.countdownTime > 0) anal.duration(new Date().valueOf() - OctMethods.prompt.countdownTime); }, startPayloadTimer: function(payloadDelay){ // Similar, but not identical, to startCountdown() $("#add_time_container").hideSafe(); $("#payload_acknowledge_container").showSafe(); $("#runtime_controls_container").showSafe(); OctMethods.prompt.countdownTime = new Date().valueOf(); // no need to create another countdownTime variable; can use the same one as regular countdown OctMethods.prompt.payloadDelay = payloadDelay; OctMethods.prompt.payloadTimerTick(); clearInterval(OctMethods.prompt.payloadTimerInterval); OctMethods.prompt.payloadTimerInterval = setInterval( OctMethods.prompt.payloadTimerTick, OctMethods.prompt.countdownDelay ); }, payloadTimerTick: function(){ // Similar, but not identical, to countdownTick() var elapsed = new Date().valueOf() - OctMethods.prompt.countdownTime; var remaining = (OctMethods.prompt.payloadDelay - elapsed); if(remaining<=0) { clearInterval(OctMethods.prompt.countdownInterval); $("#seconds_remaining").text("---"); }else{ $("#seconds_remaining").text((remaining/1000).toFixed(2)); } }, askForEnroll: function(program){ if(!viewModel.authUser()){ utils.alert(oo_translations["students.enroll.p1"]); return; } if(confirm( oo_translations["students.enroll.p2"] + "\n\nenroll('default')\n\n" + oo_translations["students.enroll.p3"])){ OctMethods.socket.enroll(program); viewModel.authUser().program = program; // note: this is not observable } }, addTime: function() { OctMethods.prompt.extraTime += OctMethods.prompt.countdownExtraTime; OctMethods.socket.addTime(); anal.extraTime(); }, acknowledgePayload: function() { // Acknowledging the payload resets the countdown on the server. OctMethods.prompt.endCountdown(); OctMethods.prompt.startCountdown(); OctMethods.socket.acknowledgePayload(); anal.acknowledgePayload(); } }, // Prompt Callback Funcions promptListeners: { command: function(){ var cmd = OctMethods.prompt.instance.getValue(); // Check if this command is a front-end command var enrollRegex = /^enroll\s*\(['"“‘”’]([^'"“‘”’]+)['"“‘”’]\).*$/; var updateStudentsRegex = /^update_students\s*\(['"“‘”’]([^'"“‘”’]+)['"“‘”’]\).*$/; var pingRegex = /^ping$/; var program; if(enrollRegex.test(cmd)){ program = cmd.match(enrollRegex)[1]; OctMethods.prompt.askForEnroll(program); OctMethods.prompt.clear(); }else if(updateStudentsRegex.test(cmd)){ program = cmd.match(updateStudentsRegex)[1]; OctMethods.socket.updateStudents(program); OctMethods.prompt.clear(); }else if(pingRegex.test(cmd)) { OctMethods.console.command(cmd, true); OctMethods.socket.ping(); OctMethods.prompt.clear(); }else{ OctMethods.console.command(cmd); OctMethods.prompt.clear(); } anal.command(cmd); }, signal: function(){ // Trigger both a signal and an empty command upstream. The empty command will sometimes help if, for any reason, the "prompt" message was lost in transit. // This could be slightly improved by adding the empty command elsewhere in the stack, to reduce the number of packets that need to be sent. OctMethods.socket.signal(); OctMethods.socket.command(""); anal.sigint(); }, historyUp: function(prompt){ var history = OctMethods.prompt.history; if (OctMethods.prompt.index == history.length-1){ history[history.length-1] = prompt.getValue(); } if (OctMethods.prompt.index > 0){ OctMethods.prompt.index -= 1; prompt.setValue(history[OctMethods.prompt.index]); prompt.getSelection().clearSelection(); } }, historyDown: function(prompt){ var history = OctMethods.prompt.history; if (OctMethods.prompt.index < history.length-1){ OctMethods.prompt.index += 1; prompt.setValue(history[OctMethods.prompt.index]); prompt.getSelection().clearSelection(); } }, keyFocus: function(e){ e.preventDefault(); OctMethods.prompt.focus(); }, permalink: function(){ var cmd = $(this).text(); window.location.hash = "cmd=" + encodeURIComponent(cmd); } }, // Socket Methods socket: { instance: null, sessCode: null, isExited: false, signal: function(){ return OctMethods.socket.emit("signal", {}); }, command: function(cmd){ return OctMethods.socket.emit("data", { data: cmd }); }, save: function(octfile){ return OctMethods.socket.emit("save", { filename: octfile.filename(), content: octfile.content() }); }, rename: function(octfile, newName){ return OctMethods.socket.emit("rename", { filename: octfile.filename(), newname: newName }); }, deleteit: function(octfile){ return OctMethods.socket.emit("delete", { filename: octfile.filename() }); }, binary: function(octfile){ return OctMethods.socket.emit("binary", { filename: octfile.filename() }); }, enroll: function(program){ return OctMethods.socket.emit("enroll", { program: program }); }, updateStudents: function(program){ return OctMethods.socket.emit("update_students", { program: program }); }, unenrollStudent: function(user){ return OctMethods.socket.emit("oo.unenroll_student", { userId: user._id }); }, reenrollStudent: function(user, newProgram){ return OctMethods.socket.emit("oo.reenroll_student", { userId: user._id, program: newProgram }); }, ping: function() { return OctMethods.socket.emit("oo.ping", { startTime: new Date().valueOf() }); }, refresh: function(){ return OctMethods.socket.emit("refresh", {}); }, toggleSharing: function(enabled){ return OctMethods.socket.emit("oo.toggle_sharing", { enabled: enabled }); }, addTime: function() { return OctMethods.socket.emit("oo.add_time", {}); }, acknowledgePayload: function() { return OctMethods.socket.emit("oo.acknowledge_payload", {}); }, setPassword: function(password) { return OctMethods.socket.emit("oo.set_password", { new_pwd: password }); }, createBucket: function(bucket) { return OctMethods.socket.emit("oo.create_bucket", { filenames: ko.utils.arrayMap(bucket.files(), function(octfile){ return octfile.filename(); }), main: bucket.mainFilename(), butype: bucket.butype(), base_bucket_id: bucket.base_bucket_id(), shortlink: bucket.shortlink(), }); }, deleteBucket: function(bucket) { return OctMethods.socket.emit("oo.delete_bucket", { bucket_id: bucket.id() }); }, changeBucketShortlink: function(bucket, newShortlink) { return OctMethods.socket.emit("oo.change_bucket_shortlink", { old_shortlink: bucket.shortlink(), new_shortlink: newShortlink, }); }, generateZip: function() { return OctMethods.socket.emit("oo.generate_zip", {}); }, emit: function(message, data){ if (!OctMethods.socket.instance || !OctMethods.socket.instance.connected) { console.log("Socket Closed", message, data); return false; } OctMethods.socket.instance.emit(message, data); return true; }, reconnect: function(){ OctMethods.load.showLoader(); OctMethods.load.startPatience(); OctMethods.socket.isExited = false; return OctMethods.socket.emit("oo.reconnect", {}); } }, // Socket Callback Functions socketListeners: { subscribe: function(socket) { socket.on("data", OctMethods.socketListeners.data); socket.on("alert", OctMethods.socketListeners.alert); socket.on("prompt", OctMethods.socketListeners.prompt); socket.on("saved", OctMethods.socketListeners.saved); socket.on("renamed", OctMethods.socketListeners.renamed); socket.on("deleted", OctMethods.socketListeners.deleted); // TODO: Stop this event from operating on everyone in a shared workspace socket.on("binary", OctMethods.socketListeners.binary); socket.on("oo.authuser", OctMethods.socketListeners.authuser); socket.on("oo.wsuser", OctMethods.socketListeners.wsuser); // The inconsistent naming convention here ("user" vs. "filelist") is for backwards compatibility. At some point I would like to rename this and other events all the way through the stack. socket.on("user", OctMethods.socketListeners.filelist); socket.on("fileadd", OctMethods.socketListeners.fileadd); socket.on("plotd", OctMethods.socketListeners.plotd); socket.on("plote", OctMethods.socketListeners.plote); socket.on("ctrl", OctMethods.socketListeners.ctrl); socket.on("workspace", OctMethods.socketListeners.vars); socket.on("sesscode", OctMethods.socketListeners.sesscode); socket.on("init", OctMethods.socketListeners.init); socket.on("files-ready", OctMethods.socketListeners.filesReady); socket.on("destroy-u", OctMethods.socketListeners.destroyu); socket.on("disconnect", OctMethods.socketListeners.disconnect); socket.on("reload", OctMethods.socketListeners.reload); socket.on("instructor", OctMethods.socketListeners.instructor); socket.on("bucket-info", OctMethods.socketListeners.bucketInfo); socket.on("bucket-created", OctMethods.socketListeners.bucketCreated); socket.on("bucket-deleted", OctMethods.socketListeners.bucketDeleted); socket.on("all-buckets", OctMethods.socketListeners.allBuckets); socket.on("oo.create-bucket-error", OctMethods.socketListeners.createBucketError); socket.on("oo.change-bucket-shortlink-response", OctMethods.socketListeners.changeBucketShortlinkResponse); socket.on("oo.pong", OctMethods.socketListeners.pong); // Flavors are no longer supported: // socket.on("oo.flavor-list", OctMethods.socketListeners.flavorList); // socket.on("oo.touch-flavor", OctMethods.socketListeners.touchFlavor); socket.on("restart-countdown", OctMethods.socketListeners.restartCountdown); socket.on("change-directory", OctMethods.socketListeners.changeDirectory); socket.on("edit-file", OctMethods.socketListeners.editFile); socket.on("payload-paused", OctMethods.socketListeners.payloadPaused); }, data: function(data){ switch(data.type){ case "stdout": OctMethods.console.write(data.data); break; case "stderr": OctMethods.console.writeError(data.data); break; case "url": OctMethods.console.writeUrl(data.url, data.linkText); break; case "exit": console.log("exit status: " + JSON.stringify(data.code)); break; default: console.log("unknown data type: " + data.type); } }, alert: function(message) { utils.alert(message); }, prompt: function(data){ var lineNumber = data.line_number || 0; // Turn on the input prompt and set the current line number OctMethods.prompt.currentLine = lineNumber; OctMethods.prompt.enable(); // Perform other cleanup logic if(OctMethods.editor.running){ if(lineNumber > 0){ OctMethods.editor.running = false; }else{ OctMethods.prompt.focus(); } }else if(isMobile && lineNumber>1){ OctMethods.prompt.focus(); setTimeout(function(){ // Does not quite work window.scrollTo(0,document.body.scrollHeight); }, 500); }else if(!isMobile){ OctMethods.prompt.focus(); } }, saved: function(data){ if (!data.success) return; var octfile = viewModel.getOctFileFromName(data.filename); if (!octfile) return; if (octfile.md5() === data.md5sum) { octfile.savedContent(octfile.content()); } else { console.log("Mismatched MD5! Local:", octfile.md5(), "Server:", data.md5sum); } }, renamed: function(data){ var oldname = data.oldname, newname = data.newname; var octfile = viewModel.getOctFileFromName(oldname); if(!octfile) return; // Rename the file throughout the schema octfile.filename(newname); allOctFiles.sort(OctFile.sorter); }, deleted: function(data){ var octfile = viewModel.getOctFileFromName(data.filename); if(!octfile) return; if (viewModel.openFile() === octfile) { OctMethods.editor.close(); } OctMethods.editor.remove(octfile); }, binary: function(data){ // Attempt to download the file console.log("Downloading binary file", data.filename); var blob = b64ToBlob(data.base64data, data.mime); return download(blob, data.filename); }, authuser: function(data){ data = data && data.user; if (!OctMethods.editor.seenAuthUser) { OctMethods.editor.seenAuthUser = true; // Ads setup if (data && data.adsDisabled) { $("#abox").hideSafe(); $("#main").css("top", 0); $("#main").css("right", 0); if (window.oo_disabledAds) { window.oo_disabledAds(); } } else if (window.oo_enableAds) { window.oo_enableAds(); } if (!data) { return; } // Trigger Knockout data.name = data.name || data.displayName; viewModel.authUser(data); // Set up the UI onboarding.showUserPromo(data); onboarding.hideScriptPromo(); onboarding.hideBucketPromo(); // Welcome Back? var welcome_back_ms = parseInt("86400000!config.client.welcome_back_ms"); if (new Date() - new Date(data.last_activity) >= welcome_back_ms) { $("#welcome_back").showSafe(); anal.welcomeback(); } // Analytics anal.signedin(); } }, wsuser: function(data) { data = data && data.user; if (!OctMethods.editor.seenWsUser) { OctMethods.editor.seenWsUser = true; if (!data) { return; } // Trigger Knockout data.name = data.name || data.displayName; viewModel.currentUser(data); // Legal runtime and other user settings OctMethods.prompt.legalTime = data.legalTime; OctMethods.prompt.countdownExtraTime = data.countdownExtraTime; OctMethods.prompt.countdownRequestTime = data.countdownRequestTime; viewModel.countdownExtraTimeSeconds(data.countdownExtraTime/1000); } }, filelist: function(data){ // Load files if (!data.success) { OctMethods.load.callback(); return utils.alert(data.message); } if (allOctFiles().length === 0) { $.each(data.files, function(filename, filedata){ if(filedata.isText){ OctMethods.editor.add(filename, Base64.decode(filedata.content)); }else{ OctMethods.editor.addNameOnly(filename); } }); // Set up the UI $("#open_container").showSafe(); $("#files_container").showSafe(); if (!OctMethods.vars.bucketId && !OctMethods.vars.wsId) { onboarding.showSyncPromo(); } // Fire a window "resize" event to make sure everything adjusts, // like the ACE editor in the prompt var evt = document.createEvent("UIEvents"); evt.initUIEvent("resize", true, false, window, 0); window.dispatchEvent(evt); // If we are in a bucket, auto-open the main file. if (viewModel.currentBucket() && viewModel.currentBucket().main()) { var filename = viewModel.currentBucket().mainFilename(); var octfile = viewModel.getOctFileFromName(filename); if (octfile) { octfile.open(); } } } else { // If the files were already loaded, update the saved content. This will make files show as unsaved if they are out-of-sync with the server. Don't do anything more drastic, like requesting a file save, because the user might not want a file save in the case of a filelist event being emitted when someone joins a shared workspace session. Also note that this could have a race condition if a save was performed after the files were read from the server; simply marking the file as unsaved is harmless and won't cause conflicts. $.each(data.files, function(filename, filedata){ if (!filedata.isText) return; var octfile = viewModel.getOctFileFromName(filename); if (octfile) { octfile.savedContent(Base64.decode(filedata.content)); } }); } }, fileadd: function(data){ if(data.isText){ var octFile = OctMethods.editor.add(data.filename, Base64.decode(data.content)); OctMethods.editor.open(octFile); }else{ OctMethods.editor.addNameOnly(data.filename); } }, plotd: function(data){ // plot data transmission var plot = getOrMakePlotById(data.id); plot.addData(data.content); console.log("Received data for plot ID "+data.id); }, plote: function(data){ // plot data complete var plot = getOrMakePlotById(data.id); plot.lineNumber(data.command_number - 1); plot.complete(true); if(data.md5 !== plot.md5()){ // should never happen console.log("MD5 discrepancy!"); console.log(data); console.log(plot.md5()); } }, ctrl: function(data){ // command from shell console.log("Received ctrl '", data.command, "' from server"); if(data.command === "clc"){ OctMethods.console.clear(); }else if(data.command.substr(0,4) === "url="){ OctMethods.console.writeUrl(data.command.substr(4)); }else if(data.command.substr(0,6) === "enroll"){ OctMethods.prompt.askForEnroll(data.command.substr(7)); } }, vars: function(data){ // update variables koTakeArray(Var, vars, "symbol", data.vars, "symbol"); vars.sort(Var.sorter); }, sesscode: function(data){ if (OctMethods.socket.sessCode === data.sessCode) { // Reconnected and matched to our original session. console.log("Restored connection to:", data.sessCode); } else { console.log("SESSCODE:", data.sessCode); OctMethods.socket.sessCode = data.sessCode; } }, reload: function(){ window.location.reload(); }, instructor: function(data){ data.users.forEach(function(user){ user.shareUrl = window.location.origin + window.location.pathname + "?s=" + user.share_key; }); viewModel.instructorPrograms.push(data); }, bucketInfo: function(data){ viewModel.currentBucket(Bucket.fromBucketInfo(data)); }, bucketCreated: function(data) { var bucket = Bucket.fromBucketInfo(data.bucket); // To stay on this page, the following lines should be run, but since we are always redirecting, they are not necessary and cause the screen to flash. // viewModel.allBuckets.push(bucket); // viewModel.newBucket(null); window.location.href = bucket.url(); }, bucketDeleted: function(data) { var bucket = ko.utils.arrayFirst(viewModel.allBuckets(), function(bucket) { return bucket.id() === data.bucket_id; }, null); viewModel.allBuckets.remove(bucket); }, allBuckets: function(data) { // N-squared loop, but it should be small enough not to be an issue $.each(data.buckets, function(i, bucketInfo) { var found = false; $.each(viewModel.allBuckets(), function(j, bucket){ if (bucket.id() === bucketInfo.bucket_id) { found = true; return false; // break } }); if (!found) { viewModel.allBuckets.push(Bucket.fromBucketInfo(bucketInfo)); } }); }, createBucketError: function(data) { if (data.type === "invalid-shortlink") { alert(oo_translations["buckets.error1"]); } else if (data.type === "duplicate-key") { alert(oo_translations["buckets.error2"] + "\n\n" + Object.values(data.data)[0]); } viewModel.newBucket().showCreateButton(true); }, changeBucketShortlinkResponse: function(data) { if (data.success) { if (viewModel.currentBucket().id() === data.bucket.bucket_id) { viewModel.currentBucket().shortlink(data.bucket.shortlink); } else { console.error("Inconsistent bucket:", viewModel.currentBucket(), data.bucket); } } else if (data.type === "invalid-shortlink") { alert(oo_translations["buckets.error1"]); } else if (data.type === "duplicate-key") { alert(oo_translations["buckets.error2"] + "\n\n" + Object.values(data.data)[0]); } }, pong: function(data) { var startTime = parseInt(data.startTime); var endTime = new Date().valueOf(); OctMethods.console.write(oo_translations["console.pingtime#label"] + " " + (endTime-startTime) + "ms\n"); OctMethods.prompt.enable(); OctMethods.prompt.focus(); }, restartCountdown: function(){ // TODO: Is this method dead? OctMethods.prompt.startCountdown(); }, changeDirectory: function(data) { viewModel.cwd(data.dir); }, editFile: function(data) { if (!data || !data.file) return; var match = data.file.match(/^\/home\/[^/]+\/(.*)$/); if (!match) return; var filename = match[1]; var octfile = viewModel.getOctFileFromName(filename); if (!octfile) { // New file octfile = OctMethods.editor.create(filename); } if (octfile) { octfile.open(); } }, payloadPaused: function(data){ OctMethods.prompt.endCountdown(); OctMethods.prompt.startPayloadTimer(data.delay); // Show the notification message after a small delay in order to let the output buffers flush first. setTimeout(function(){ OctMethods.console.writeError("\n" + oo_translations["console.payload#alert"] + "\n"); }, parseInt("105!config.session.payloadMessageDelay")); }, init: function(){ // Regular session or shared session? if (OctMethods.vars.wsId) { OctMethods.socket.emit("init", { action: "workspace", info: OctMethods.vars.wsId, skipCreate: OctMethods.socket.isExited, }); }else if(OctMethods.vars.studentId){ OctMethods.socket.emit("init", { action: "student", info: OctMethods.vars.studentId, skipCreate: OctMethods.socket.isExited, }); }else if (OctMethods.vars.bucketId){ OctMethods.socket.emit("init", { action: (viewModel.purpose() === "bucket") ? "bucket" : "project", info: OctMethods.vars.bucketId, sessCode: OctMethods.socket.sessCode, skipCreate: OctMethods.socket.isExited, }); }else{ OctMethods.socket.emit("init", { action: "session", sessCode: OctMethods.socket.sessCode, skipCreate: OctMethods.socket.isExited, }); } // If skipCreate, hide the loader now. (If a session is being made for us, wait to hide the loader until the session is ready.) if (OctMethods.socket.isExited) { OctMethods.load.hideLoader(); } }, filesReady: function(message) { // hide the coverall loading div if necessary, and perform other initialization tasks OctMethods.load.callback(message); }, destroyu: function(message){ OctMethods.console.writeError(oo_translations["console.exited#alert"] + " " + message + "\n"); OctMethods.console.writeRestartBtn(); OctMethods.socket.isExited = true; OctMethods.load.hideLoader(); // Clean up UI OctMethods.prompt.disable(); OctMethods.prompt.endCountdown(); }, disconnect: function(){ if (!OctMethods.socket.isExited) { OctMethods.console.writeError(oo_translations["console.reconnecting#alert"] + "\n"); } // Clean up UI OctMethods.prompt.disable(); OctMethods.prompt.endCountdown(); OctMethods.load.showLoader(); }, }, // Editor Methods editor: { instance: null, defaultFilename: "my_script.m", defaultContent: "disp(\"" + oo_translations["newfile.helloworld"] + "\");\n", running: false, // AuthUser is the user who is currently signed in; WsUser is the user who owns the currently loaded workspace. seenAuthUser: false, seenWsUser: false, bucketWarned: false, save: function(octfile){ if (viewModel.purpose() === "bucket" && !OctMethods.editor.bucketWarned) { utils.alert(oo_translations["console.readonly#alert@2"]+"\n\n"+(viewModel.currentBucket()&&viewModel.currentBucket().shortlink())); OctMethods.editor.bucketWarned = true; } return OctMethods.socket.save(octfile); }, add: function(filename, content){ var octfile = new OctFile(filename, content, true); allOctFiles.push(octfile); allOctFiles.sort(OctFile.sorter); return octfile; }, addNameOnly: function(filename){ var octfile = new OctFile(filename, "", false); allOctFiles.push(octfile); allOctFiles.sort(OctFile.sorter); return octfile; }, create: function(filename){ // check to see if the file already exists if (viewModel.fileNameExists(filename)) { return false; } // check for valid filename if(!OctFile.regexps.filename.test(filename)){ return false; } var octfile = OctMethods.editor.add( filename, OctMethods.editor.defaultContent); OctMethods.editor.save(octfile); return octfile; }, remove: function(octfile){ allOctFiles.remove(octfile); }, deleteit: function(octfile){ return OctMethods.socket.deleteit(octfile); }, run: function(octfile){ var cmd = octfile.command(); if(!cmd) return false; OctMethods.console.command(cmd); OctMethods.editor.running = true; anal.runfile(); return true; }, rename: function(octfile){ var oldName = octfile.filename(); var newName = prompt(oo_translations["rename.label"], oldName); if (!newName || oldName === newName) return false; if (viewModel.fileNameExists(newName)){ utils.alert(oo_translations["rename.alert"]); return false; } return OctMethods.socket.rename(octfile, newName); }, download: function(octfile){ // Two cases: front-end text file or back-end binary file. if(octfile.editable){ // If it's a text file, we can download it now var mime = "text/x-octave;charset=utf-8"; var blob = new Blob([octfile.content()], { type: mime }); return download(blob, octfile.filename()); }else{ // If it's a binary file, we have to request it from the server return OctMethods.socket.binary(octfile); } }, print: function(octfile){ // Make a new window and a temporary document object var w = window.open(); var doc = $("
"); // Add a title line var h1 = $("

"); h1.append(octfile.filename()); h1.css("font", "bold 14pt/14pt 'Trebuchet MS',Verdana,sans-serif"); h1.css("margin", "6pt"); doc.append(h1); // Create the Ace highlighter var highlight = aceStaticHighlight.render( octfile.content(), new (require("ace/mode/octave").Mode)(), require("ace/theme/crimson_editor") ); // Create the Ace stylesheet var ss = $(""); ss.append(highlight.css); // Append the Ace highlighter and stylesheet var editorDiv = $("
"); editorDiv.append(highlight.html); doc.append(ss); doc.append(editorDiv); // Add a credit line at the bottom var creditDiv = $("
"); creditDiv.append(oo_translations["print.p1"] + " " + (viewModel.authUser() || { name: "Anonymous" }).name); creditDiv.append("
"); creditDiv.append(oo_translations["print.p2"]); creditDiv.append("
"); creditDiv.append("http://octave-online.net"); creditDiv.css("font", "10pt/10pt 'Trebuchet MS',Verdana,sans-serif"); creditDiv.css("text-align", "right"); creditDiv.css("margin-top", "16pt"); doc.append(creditDiv); // Add the document data to the window w.document.body.innerHTML += doc.html(); // Trigger Print w.window.print(); }, open: function(octfile){ viewModel.openFile(octfile); }, close: function(){ viewModel.openFile(null); }, reset: function(){ viewModel.openFile(null); allOctFiles.removeAll(); }, }, // Editor Callback Functions editorListeners: { newCB: function(){ var filename = OctMethods.editor.defaultFilename; // do..while to protect against duplicate file names do{ filename = prompt(oo_translations["newfile.label"], filename); } while(filename && !OctMethods.editor.create(filename)); }, refresh: function(){ if(confirm(oo_translations["console.refresh#alert"])){ OctMethods.editor.reset(); OctMethods.socket.refresh(); } }, info: function(){ anal.sitecontrol("showfilehistory"); $("#file_history_box").showSafe(); }, run: function(){ OctMethods.editor.run(viewModel.openFile()); }, keyRun: function(e){ e.preventDefault(); if(viewModel.openFile()){ OctMethods.editor.run(viewModel.openFile()); } } }, load: { firstConnection: true, loaderVisible: true, bePatientTimeout: null, callback: function(message){ OctMethods.load.hideLoader(); var initCmd = ""; // As soon as files are loaded for the first time, execute the .octaverc if it is present // GNU Octave normally does this automatically, but we pre-start the processes against a clean directory, so .octaverc is not present when GNU Octave starts up // Do this on the client so that the UI reflects that a command is being run // Do this every time we receive a files-ready command, indicating that a new session has started if (message && message.hasOctaverc) { initCmd += "source(\".octaverc\"); "; } if(OctMethods.load.firstConnection){ OctMethods.load.firstConnection = false; // UI setup $("#type_here").showSafe(); $("#agpl_icon").showSafe(); $("#plot_opener").hideSafe(); $("#vars_panel").showSafe(); // Initial bucket command if (viewModel.currentBucket() && viewModel.currentBucket().mainFilename() && viewModel.currentBucket().mainFilename() !== ".octaverc") { initCmd += "source(\"" + viewModel.currentBucket().mainFilename() + "\"); "; } // Evaluate the query string command try{ var hashParams = new URLSearchParams(new URL(window.location.href).hash.slice(1)); var purlCmd = hashParams.get("cmd"); if (purlCmd) initCmd += purlCmd; }catch(e){ console.log(e); } } if(initCmd){ OctMethods.console.command(initCmd); } }, showLoader: function(){ if (OctMethods.load.loaderVisible) return; OctMethods.load.loaderVisible = true; $("#site_loading").showSafe(); }, hideLoader: function(){ if (!OctMethods.load.loaderVisible) return; OctMethods.load.loaderVisible = false; OctMethods.load.stopPatience(); $("#site_loading").fadeOutSafe(500); }, startPatience: function(){ OctMethods.load.stopPatience(); OctMethods.load.bePatientTimeout = setTimeout(function(){ $("#site_loading_patience").showSafe(); anal.patience(); OctMethods.load.bePatientTimeout = setTimeout(function(){ $("#site_loading_patience").hideSafe(); $("#site_loading_more_patience").showSafe(); OctMethods.load.bePatientTimeout = null; }, 35000); }, 10000); }, stopPatience: function(){ $("#site_loading_patience").hideSafe(); $("#site_loading_more_patience").hideSafe(); if (!OctMethods.load.bePatientTimeout) return; clearTimeout(OctMethods.load.bePatientTimeout); OctMethods.load.bePatientTimeout = null; } }, // Other accessor properties ko: { viewModel: viewModel, allOctFiles: allOctFiles, availableSkins: availableSkins }, vars: { wsId: null, studentId: null, bucketId: null, } }; viewModel.countdownExtraTimeSeconds(OctMethods.prompt.countdownExtraTime/1000); // Expose exports.console = OctMethods.console; exports.prompt = OctMethods.prompt; exports.promptListeners = OctMethods.promptListeners; exports.plot = OctMethods.plot; exports.socket = OctMethods.socket; exports.socketListeners = OctMethods.socketListeners; exports.editor = OctMethods.editor; exports.editorListeners = OctMethods.editorListeners; exports.load = OctMethods.load; exports.ko = OctMethods.ko; exports.vars = OctMethods.vars; }); // AMD Define ================================================ FILE: client/app/js/detectmobilebrowser.js ================================================ /* eslint-disable */ // based on http://detectmobilebrowsers.com/ define(function(){ var ua = navigator.userAgent || navigator.vendor || window.opera; var regex1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i; var regex2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i; return regex1.test(ua) || regex2.test(ua.substr(0,4)); }); ================================================ FILE: client/app/js/download.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(["filesaver"], function(saveAs){ // FileSaver does not support IE9. return function(blob, filename){ if(!saveAs(blob, filename)){ alert("File download is unfortunately not supported in your " + "browser.\n\nConsider taking a screenshot to save your plot " + "or manually copy/paste your script file into an editor."); return false; } return true; }; }); ================================================ FILE: client/app/js/flex-resize.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(["jquery", "knockout"], function($, ko){ var active = false, obsArr, xRef, index, element; function cbmove(e){ if (!active) return; e.preventDefault(); var arr = obsArr(); var i, j; // Try to be a little bit smart as to how much of a jump we need. // This calculation is an approximation only. The second section of // this function will fix any errors. var windowWidth = $(window).width(); var flexWidth = arr.reduce(function(s,x){ return s+x; }, 0); var dist = e.pageX - $(element).offset().left - xRef; var jump = Math.round(windowWidth/flexWidth*dist); var m; if(jump > 0){ // Move Right for(i=index; jump>0 && i-1; i--){ m = Math.min(Math.abs(jump), arr[i]); arr[i] -= m; arr[index] += m; jump += m; } } obsArr.valueHasMutated(); // Now do fine-tuning // Limit to 25 iterations to help prevent infinite loops // Move Right for(i=0; e.pageX - $(element).offset().left > xRef && i<25; i++){ for(j=index; arr[j]===0 && j-1; j--){/* no-op */} if (j===-1) break; arr[j] -= 1; arr[index] += 1; obsArr.valueHasMutated(); } // Update the reference point xRef = e.pageX - $(element).offset().left; } function cbdown(e, _obsArr){ active = true; obsArr = _obsArr; xRef = e.pageX - $(this).offset().left; index = $(this).data("index"); element = this; $(document).on("mousemove", cbmove); $(document).on("touchmove", cbmove); $(document).on("mouseup", cbup); $(document).on("touchend", cbup); } function cbup(){ active = false; $(document).off("mousemove", cbmove); $(document).off("touchmove", cbmove); $(document).off("mouseup", cbup); $(document).off("touchend", cbup); // Fire a window "resize" event to make sure everything adjusts, // like the ACE editor var evt = document.createEvent("UIEvents"); evt.initUIEvent("resize", true, false, window, 0); window.dispatchEvent(evt); } ko.bindingHandlers.resizeFlex = { init: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { var obj = valueAccessor(); var index = ko.utils.unwrapObservable(obj.index); if (index === 0) return; var handle = $("
"); handle.addClass("handle"); handle.data("index", index); handle.on("mousedown", function(e){ cbdown.call(handle, e, obj.group); }); handle.on("touchstart", function(e){ cbdown.call(handle, e, obj.group); }); window.viewModel.flex.shown.subscribe(function(newValue){ handle.toggleSafe(newValue); }); handle.hideSafe(); $(element).append(handle); }, update: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { var obj = valueAccessor(); var sizes = ko.utils.unwrapObservable(obj.group); var index = ko.utils.unwrapObservable(obj.index); var styleString = sizes[index] + "px"; if ($(element).css("flex-basis") !== styleString) { $(element).css("flex-basis", styleString); } } }; // This part of the code is not portable $(document).ready(function(){ $(document).on("mouseover", function(e){ if (!window.viewModel) return; window.viewModel.flex.shown( active || $(e.target).attr("data-hover") === "flex" || $(e.target).hasClass("handle")); }); }); }); ================================================ FILE: client/app/js/ko-ace.js ================================================ // Much of this file originates from https://github.com/probonogeek/knockout-ace/blob/master/knockout-ace.js define(["knockout", "ace/ace"], function(ko, ace){ ko.bindingHandlers.ace = { init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { var obj = ko.utils.unwrapObservable(valueAccessor()); var text = ko.utils.unwrapObservable(obj.text); var skin = ko.utils.unwrapObservable(obj.skin); var wrap = ko.utils.unwrapObservable(obj.wrap); var octfile = ko.utils.unwrapObservable(obj.octfile); // Make the editor var editor = ace.edit(element.id); editor.setTheme(skin.aceTheme); editor.getSession().setMode("ace/mode/octave"); editor.getSession().setUseWrapMode(wrap); editor.setValue(text); editor.gotoLine(0); editor.commands.addCommand({ name: "save", bindKey: { mac: "Command-S", win: "Ctrl-S" }, exec: octfile.save, readOnly: false }); editor.setOptions({ enableBasicAutocompletion: true }); editor.setBehavioursEnabled(false); editor.focus(); setTimeout(editor.resize.bind(editor), 0); // Bind to Knockout changes var onAceChange = function(){ var _obj = valueAccessor(); if (ko.isWriteableObservable(_obj.text)) { _obj.text(editor.getValue()); } }; editor.getSession().on("change", onAceChange); // Attach OT to the editor instance var otClient = octfile.getOtClient(); if (otClient) otClient.attachEditor(editor); // Save reference if (bindingContext.editor) { console.log("WARNING: bindingContext.editor already set"); } bindingContext.editor = editor; // Destroy Handler ko.utils.domNodeDisposal.addDisposeCallback(element, function() { editor.getSession().off("change", onAceChange); if (otClient) otClient.attachEditor(null); delete bindingContext.editor; editor.destroy(); }); }, update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { var obj = ko.utils.unwrapObservable(valueAccessor()); var text = ko.utils.unwrapObservable(obj.text); var skin = ko.utils.unwrapObservable(obj.skin); var wrap = ko.utils.unwrapObservable(obj.wrap); var editor = bindingContext.editor; if(editor.getValue() !== text){ editor.setValue(text); editor.gotoLine(0); } if(editor.getTheme() !== skin.aceTheme){ editor.setTheme(skin.aceTheme); } if(editor.getSession().getUseWrapMode() !== wrap){ editor.getSession().setUseWrapMode(wrap); } } }; }); ================================================ FILE: client/app/js/ko-flash.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define("ko-flash", ["knockout", "jquery"], function(ko, $){ ko.bindingHandlers.flash = { init: function(element /*, valueAccessor, allBindings, viewModel, bindingContext */) { // This will be called when the binding is first applied to an element // Set up any initial state, event handlers, etc. here $(element).addClass("transition-property-bgcolor"); }, update: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { // This will be called once when the binding is first applied to an element, // and again whenever any observables/computeds that are accessed change // Update the DOM element based on the supplied values here. // Prevent this handler from being garbage collected $(element).attr("dummy-attribute", ko.unwrap(valueAccessor())); $(element).removeClass("transition-duration-medium"); $(element).addClass("transition-duration-instant"); $(element).addClass("ko-flash"); setTimeout(function(){ $(element).removeClass("transition-duration-instant"); $(element).addClass("transition-duration-medium"); $(element).removeClass("ko-flash"); }, 500); } }; }); ================================================ FILE: client/app/js/ko-takeArray.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define("ko-takeArray", ["knockout"], function(ko){ return function(ObsClass, obsArr, objKey, dataArr, dataKey){ var obsKeys = ko.utils.arrayMap(obsArr(), function(v){ return v[objKey](); }); var dataKeys = ko.utils.arrayMap(dataArr, function(v){ return v[dataKey]; }); var intersection = []; var obsOnly = []; var dataOnly = []; ko.utils.arrayForEach(obsKeys, function(k, i1){ var i2 = dataKeys.indexOf(k); if(i2 !== -1){ // Found a Match intersection.push([obsArr()[i1], dataArr[i2]]); }else{ // Item to Remove obsOnly.push(obsArr()[i1]); } }); ko.utils.arrayForEach(dataKeys, function(k, i2){ var i1 = obsKeys.indexOf(k); if(i1 === -1){ // Item to Add dataOnly.push(dataArr[i2]); } }); // Update the Matches ko.utils.arrayForEach(intersection, function(vv){ var obs = vv[0], dat = vv[1]; obs.take(dat); }); // Remove expired values ko.utils.arrayForEach(obsOnly, function(obs){ obsArr.remove(obs); }); // Add new values ko.utils.arrayForEach(dataOnly, function(dat){ var obs = new ObsClass(); obs.take(dat); obsArr.push(obs); }); }; }); ================================================ FILE: client/app/js/modernizr-201406b.js ================================================ /* eslint-disable */ /* Modernizr 2.6.2 (Custom Build) | MIT & BSD * Build: http://modernizr.com/download/#-fontface-canvas-svg-teststyles */ ;window.Modernizr=function(a,b,c){function v(a){i.cssText=a}function w(a,b){return v(prefixes.join(a+";")+(b||""))}function x(a,b){return typeof a===b}function y(a,b){return!!~(""+a).indexOf(b)}function z(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:x(f,"function")?f.bind(d||b):f}return!1}var d="2.6.2",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s=function(a,c,d,e){var h,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),l.appendChild(j);return h=["­",'"].join(""),l.id=g,(m?l:n).innerHTML+=h,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=f.style.overflow,f.style.overflow="hidden",f.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),f.style.overflow=k),!!i},t={}.hasOwnProperty,u;!x(t,"undefined")&&!x(t.call,"undefined")?u=function(a,b){return t.call(a,b)}:u=function(a,b){return b in a&&x(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},m.fontface=function(){var a;return s('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},m.svg=function(){return!!b.createElementNS&&!!b.createElementNS(l.svg,"svg").createSVGRect};for(var A in m)u(m,A)&&(r=A.toLowerCase(),e[r]=m[A](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)u(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},v(""),h=j=null,e._version=d,e.testStyles=s,e}(this,this.document); ================================================ FILE: client/app/js/octfile.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define([ "knockout", "require", "js/ws-shared", "jquery", "jquery.md5"], function(ko, require, WsShared, $){ var OctMethods = require("js/client"); // OctFile MVVM class function OctFile(filename, content, editable){ // the "self" variable enables us to refer to the OctFile context even when // we are programming within callback function contexts var self = this; // Main Bindings self.filename = ko.observable(filename); self.content = ko.observable(content); self.editable = editable; // Identifier: needs to be URL hash safe self.identifier = ko.computed(function(){ // Replace all nonalphanumeric characters with underscores return self.filename().replace(/\W/g, "_"); }); self.hash = ko.computed(function(){ return "#"+self.identifier(); }); // Methods relating to running the file self.baseName = ko.computed(function(){ var nameMatch = OctFile.regexps.functionname.exec(self.filename()); if (!nameMatch || nameMatch.length < 1) return false; return nameMatch[1]; }); self.isFunction = ko.computed(function(){ return OctFile.regexps.isFunction.test(self.content()); }); self.getFunctionParameters = ko.computed(function(){ if(!self.isFunction()) return false; var match = OctFile.regexps.matchParameters.exec(self.content()); if(!match || match[1]==="") return []; return match[1].split(/\s*,\s*/); }); var argumentsStore = []; self.command = function(){ if (!self.runnable()) return false; var parameters = self.getFunctionParameters(); var baseName = self.baseName(); if (parameters) { var arg; for(var i=0; i. */ // Handle the "onboarding" demonstration div define(["jquery", "js/anal", "jquery.cookie", "js/utils"], function($, anal){ var $onboarding = $("#onboarding"), $announcement = $("#announcement-box"), $scriptPromo = $("#login-promo"), $instructorPromo = $("#instructor-promo"), $syncPromo = $("#sync-promo"), $sharePromo = $("#share-promo"), $createBucketPromo = $("#create-bucket-promo"), $bucketPromo = $("#bucket-promo"), MIN_TIME = 1000; var announcementDisplay = $announcement.data("announcementDisplay"); var showAnnouncement = function() { $announcement.fadeInSafe(500); }; if ($onboarding.length) { // Check for the onboarding cookie now if ($.cookie("oo_onboarding_complete") === "true") { // Case 1: Returning user who has completed onboarding // Hide the onboarding dialog and show the announcement if required $onboarding.fadeOutSafe(500); if (announcementDisplay === "on" || announcementDisplay === "returning") { showAnnouncement(); } } else { // Case 2: New user who has not completed onboarding // Set event listeners for the onboarding div // Show the announcement after the user dismisses the onboarding $onboarding.find("[data-purpose='close']").click(function(){ $.cookie("oo_onboarding_complete", "true", { expires: MIN_TIME }); if (announcementDisplay === "on") { showAnnouncement(); } anal.dismiss("Welcome Message"); }); } } else { // Case 3: Onboarding is disabled // Show the announcement if required if (!$onboarding.length && announcementDisplay === "on") { showAnnouncement(); } } // Set up the script promo onboarding if(!$.cookie("oo_script_promo_dismissed")){ $scriptPromo.showSafe(); $scriptPromo.find("[data-purpose='close']").click(function(){ // Make this cookie expire on browser being closed $.cookie("oo_script_promo_dismissed", "true"); anal.dismiss("Sign in for Scripts"); $scriptPromo.fadeOutSafe(500); }); } // Set up the instructor promo onboarding if(!$.cookie("oo_instructor_promo_dismissed")){ $instructorPromo.showSafe(); $instructorPromo.find("[data-purpose='close']").click(function(){ $.cookie("oo_instructor_promo_dismissed", "true", { expires: MIN_TIME }); anal.dismiss("Instructors"); $instructorPromo.fadeOutSafe(500); }); } // Set the listener for create-bucket var createBucketPromoShown = false; $createBucketPromo.find("[data-purpose='close']").click(function(){ $.cookie("oo_create_bucket_promo_dismissed", "true", { expires: MIN_TIME }); anal.dismiss("Create Bucket"); $createBucketPromo.fadeOutSafe(500); createBucketPromoShown = false; }); // Expose an API var onboarding = { reset: function(){ // Delete the cookie $.cookie("oo_onboarding_complete", null); }, showUserPromo: function(data) { // Show tier upgrade screen the first time the user makes a pledge if (data.patreon && data.patreon.currently_entitled_amount_cents > 0) { if(!$.cookie("oo_new_upgrade_dismissed")){ $("#upgrade_to_tier").showSafe(); $.cookie("oo_new_upgrade_dismissed", "true", { expires: MIN_TIME }); } } }, showSyncPromo: function(){ // Set up the Octave Online Sync onboarding if(!$.cookie("oo_sync_promo_dismissed")){ $syncPromo.fadeInSafe(500); $syncPromo.find("[data-purpose='close']").click(function(){ $.cookie("oo_sync_promo_dismissed", "true", { expires: MIN_TIME }); anal.dismiss("Octave Online Sync"); $syncPromo.fadeOutSafe(500); }); } // Also use this function to set up the Share onboarding if(!$.cookie("oo_share_promo_dismissed")){ $sharePromo.fadeInSafe(500); $sharePromo.find("[data-purpose='close']").click(function(){ $.cookie("oo_share_promo_dismissed", "true", { expires: MIN_TIME }); anal.dismiss("Set up Sharing"); $sharePromo.fadeOutSafe(500); }); } }, toggleCreateBucketPromo: function(show) { if (!show && createBucketPromoShown) { $createBucketPromo.hideSafe(); createBucketPromoShown = false; } else if (show && !createBucketPromoShown && !$.cookie("oo_create_bucket_promo_dismissed")) { $createBucketPromo.fadeInSafe(500); createBucketPromoShown = true; // Hack: we need to wait until the editor is shown before repositioning setTimeout(onboarding.reposition, 0); } }, hideScriptPromo: function(){ $scriptPromo.hideSafe(); $sharePromo.hideSafe(); }, showBucketPromo: function() { $scriptPromo.hideSafe(); $bucketPromo.showSafe(); $bucketPromo.find("[data-purpose='close']").click(function(){ // Don't persist the dismissal of this one. anal.dismiss("Sign in for Buckets"); $bucketPromo.fadeOutSafe(500); }); }, hideBucketPromo: function() { $bucketPromo.hideSafe(); }, reposition: function() { var $syncButton = $("#files_toolbar_info"); var $cbucketButton = $("#editor_share"); if ($syncButton.length) { $syncPromo.css("left", ($syncButton.offset().left-2) + "px"); } if ($cbucketButton.length) { $createBucketPromo.css("left", ($cbucketButton.offset().left-2) + "px"); } } }; return onboarding; }); ================================================ FILE: client/app/js/ot-client.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(["js/ace-adapter", "ot", "js/polyfill"], function(AceAdapter, ot){ function OTClientWrapper(docId, observable){ this.id = docId; this.observable = observable || null; this.content = ""; this.attachEditor(null); } function ObservableAdapter(observable){ this.applyOperation = function(operation){ var content = observable(); content = operation.apply(content); observable(content); }; this.getValue = function(){ return observable(); }; // TODO: Use these other methods to remember where people's cursors are, // so they can be set as soon as the document is actually opened in ACE. this.getCursor = function(){}; this.setOtherCursor = function(){}; this.detach = function(){}; } OTClientWrapper.prototype.applyContent = function(){ if (!this.adapter) return; var currentContent = this.adapter.getValue(); if (currentContent === this.content) return; var op = new ot.TextOperation(); op["delete"](currentContent.length); op.insert(this.content); this.adapter.applyOperation(op); }; OTClientWrapper.prototype.initWith = function(rev, content){ this.otClient = new ot.Client(rev); this.otClient.sendOperation = this._sendOperation.bind(this); this.otClient.applyOperation = this._applyOperation.bind(this); this.content = content; this.applyContent(); }; OTClientWrapper.prototype.attachEditor = function(editor){ if (this.adapter) { this.adapter.detach(); // removes event listeners delete this.adapter; this.cursor = {}; } if (editor) { // Attach to the ACE Editor Adapter. // // Note that the ACE Editor is itself bound to the observable via // ko-ace.js, so we don't need to attach to the observable from here. this.adapter = new AceAdapter(editor); this.adapter.addEventListener("change", this._onChange.bind(this), false); this.adapter.addEventListener("cursorActivity", this._onCursor.bind(this), false); this.adapter.addEventListener("focus", this._onFocusBlur.bind(this), false); this.adapter.addEventListener("blur", this._onFocusBlur.bind(this), false); this.cursor = this.adapter.getCursor(); } else if (this.observable) { // Attach to the Knockout observable directly, enabling edit and save // actions to propogate even when the file is not open in the editor. // // TODO: Make this two-way communication by adding event listeners here. this.adapter = new ObservableAdapter(this.observable); } this.applyContent(); }; OTClientWrapper.prototype.changeDocId = function(newDocId){ this.id = newDocId; }; OTClientWrapper.prototype.destroy = function(){ this.attachEditor(null); delete this.adapter; delete this.observable; this.callbacks = {}; }; // SERVER -> OT OTClientWrapper.prototype.applyServer = function(operation){ if (this.otClient) this.otClient.applyServer(operation); }; OTClientWrapper.prototype.serverAck = function(){ this.otClient.serverAck(); }; // ACE -> OT OTClientWrapper.prototype._onChange = function(operation /*, inverse */){ this.otClient.applyClient(operation); this.content = operation.apply(this.content); }; // OT -> SERVER OTClientWrapper.prototype._sendOperation = function(revision, operation){ this.dispatchEvent("send", revision, operation); }; // OT -> ACE OTClientWrapper.prototype._applyOperation = function(operation){ if (this.adapter) this.adapter.applyOperation(operation); this.content = operation.apply(this.content); }; // CURSORS OTClientWrapper.prototype._onCursor = function(){ var cursor = this.adapter.getCursor(); if(this.cursor.position !== cursor.position || this.cursor.selectionEnd !== cursor.selectionEnd){ this.cursor = cursor; this.dispatchEvent("cursor", cursor); } }; OTClientWrapper.prototype._onFocusBlur = function(){ }; OTClientWrapper.prototype.setOtherCursor = function(){ if (!this.adapter) return; this.adapter.setOtherCursor.apply(this.adapter, arguments); }; // Begin EventTarget Implementation OTClientWrapper.prototype.addEventListener = function(event, cb){ if(!this.callbacks) this.callbacks = {}; if(!this.callbacks[event]) this.callbacks[event] = []; this.callbacks[event].push(cb); }; OTClientWrapper.prototype.removeEventListener = function(event, cb){ if(!this.callbacks || !this.callbacks[event]) return; for (var i=0; i. */ define(["js/client", "js/ot-client", "ot", "js/polyfill"], function(OctMethods, OtClient, ot){ var clients = []; function findDocWithId(docId){ for (var i = clients.length - 1; i >= 0; i--) { if (clients[i].id === docId) { return clients[i]; } } return null; } var clientListeners = { send: function(revision, operation){ OctMethods.socket.emit("ot.change", { docId: this.id, rev: revision, op: operation }); }, cursor: function(cursor){ OctMethods.socket.emit("ot.cursor", { docId: this.id, cursor: cursor }); } }; var socketListeners = { subscribe: function(socket) { socket.on("ot.doc", socketListeners.doc); socket.on("ot.broadcast", socketListeners.broadcast); socket.on("ot.ack", socketListeners.ack); socket.on("ot.cursor", socketListeners.cursor); }, doc: function(obj){ var otClient = findDocWithId(obj.docId); if (!otClient) return; otClient.initWith(obj.rev, obj.content); }, broadcast: function(obj){ var otClient = findDocWithId(obj.docId); if (!otClient) return; var op = ot.TextOperation.fromJSON(obj.ops); otClient.applyServer(op); }, ack: function(obj){ var otClient = findDocWithId(obj.docId); if (!otClient) return; otClient.serverAck(); }, cursor: function(obj){ var otClient = findDocWithId(obj.docId); if (!otClient) return; otClient.setOtherCursor(obj.cursor, "#F00", "Remote User"); } }; function create(docId, observable) { var otClient = findDocWithId(docId); if (otClient) return otClient; otClient = new OtClient(docId, observable); otClient.addEventListener("send", clientListeners.send.bind(otClient)); otClient.addEventListener("cursor", clientListeners.cursor.bind(otClient)); clients.push(otClient); return otClient; } function destroy(docId) { for (var i = clients.length - 1; i >= 0; i--) { if (clients[i].id === docId) { clients.splice(i, 1); return; } } } return { create: create, destroy: destroy, listeners: socketListeners, _clients: clients }; }); ================================================ FILE: client/app/js/polyfill.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(function(){ // Function.prototype.bind polyfill if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== "function") { // closest thing possible to the ECMAScript 5 // internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1); var fToBind = this; var fNOP = function() {}; var fBound = function() { return fToBind.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; } }); ================================================ FILE: client/app/js/runtime.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // RequireJS config for live application loads. // New require.config for live application loads. require.config({ paths: { // Social Plugins "twitter": [ "https://platform.twitter.com/widgets", "http://platform.twitter.com/widgets", "/js-default/twitter" ], "addthis": [ "https://s7.addthis.com/js/300/addthis_widget", // #pubid=ra-51d65a00598f1528 "http://s7.addthis.com/js/300/addthis_widget", // #pubid=ra-51d65a00598f1528 "/js-default/addthis" ], "uservoice": [ "https://widget.uservoice.com/{!uservoice!}", "http://widget.uservoice.com/{!uservoice!}", "/js-default/uservoice" ], "analytics": [ "https://www.google-analytics.com/analytics", "http://www.google-analytics.com/analytics", "/js-default/analytics" ], "gtag": [ "https://www.googletagmanager.com/gtag/js?id={!gtagid!}", "http://www.googletagmanager.com/gtag/js?id={!gtagid!}", "/js-default/gtag" ], "webfont": [ "https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont", "http://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont", "/js-default/webfont" ], "persona": [ "https://login.persona.org/include", "/js-default/persona" ], "recaptcha": [ "https://www.recaptcha.net/recaptcha/api", "/js-default/recaptcha" ], "fuse": [ // TODO: Make this URL configurable "https://cdn.fuseplatform.net/publift/tags/2/2356/fuse", "/js-default/fuse" ] }, shim:{ "twitter": { exports: "twttr" }, "webfont": { exports: "WebFont" }, "analytics": { exports: "ga" }, "persona": { exports: "navigator.id" } } }); // CSS shim require.css = function(url){ var link = document.createElement("link"); link.type = "text/css"; link.rel = "stylesheet"; link.href = url; document.getElementsByTagName("head")[0].appendChild(link); }; // Load from Google Fonts // NOTE: There are no fonts from Google Fonts currently in use. // require(["webfont"], function(WebFont){ // WebFont.load({ // google: { // families: ["Rambla", "Bangers"] // } // }); // }); // Load Google Analytics require(["js/anal"], function(anal){ anal.pageview(); }); // Load DejaVu Sans Mono (lower priority) setTimeout(function(){ require.css("fonts/dejavusansmono_book/stylesheet.css"); }, 250); // Load ReCAPTCHA (lower priority) setTimeout(function(){ require(["recaptcha"]); }, 250); // Load Social Bloatware (lowest priority) setTimeout(function(){ require(["uservoice"]); }, 500); ================================================ FILE: client/app/js/utils.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Assorted utility functions and polyfills define(["jquery", "knockout", "js/anal"], function($, ko, anal){ // Safe Show/Hide Functions that retain display properties like flex. $.fn.hideSafe = function(){ $(this).attr("aria-hidden", "true"); }; $.fn.showSafe = function(){ $(this).attr("aria-hidden", "false"); }; $.fn.toggleSafe = function(bool){ if(bool === true || $(this).attr("aria-hidden") === "true"){ $(this).showSafe(); return true; }else{ $(this).hideSafe(); return false; } }; $.fn.fadeInSafe = function(duration){ $(this).showSafe(); $(this).css("display", "none"); $(this).fadeIn(duration, function(){ $(this).css("display", ""); }); }; $.fn.fadeOutSafe = function(duration){ $(this).showSafe(); $(this).css("display", "block"); $(this).fadeOut(duration, function(){ $(this).hideSafe(); $(this).css("display", ""); }); }; ko.bindingHandlers.vizSafe = { init: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { $(element).toggleSafe(ko.unwrap(valueAccessor())); }, update: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { $(element).toggleSafe(ko.unwrap(valueAccessor())); } }; // Fade in/out bindings ko.bindingHandlers.fade = { init: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { $(element).toggleSafe(ko.unwrap(valueAccessor())); }, update: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { if(ko.unwrap(valueAccessor())){ $(element).fadeInSafe(250); }else{ $(element).fadeOutSafe(250); } } }; ko.bindingHandlers.fadeIn = { init: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { $(element).toggleSafe(ko.unwrap(valueAccessor())); }, update: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { if(ko.unwrap(valueAccessor())){ $(element).fadeInSafe(250); }else{ $(element).hideSafe(); } } }; ko.bindingHandlers.fadeOut = { init: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { $(element).toggleSafe(ko.unwrap(valueAccessor())); }, update: function(element, valueAccessor /*, allBindings, viewModel, bindingContext */) { if(ko.unwrap(valueAccessor())){ $(element).showSafe(); }else{ $(element).fadeOutSafe(250); } } }; // Toggle observable function ko.observable.fn.toggle = function () { var self = this; return function () { self(!self()); }; }; // Add an extender to back an observable by localStorage ko.extenders.localStorage = function(obs, key) { key = "oo:" + key; // Restore value from localStorage if (window.localStorage[key]) { obs(JSON.parse(window.atob(window.localStorage[key]))); } // Save changes to localStorage obs.subscribe(function(value){ window.localStorage[key] = window.btoa(JSON.stringify(value)); }); }; // Additional utility functions return { binarySearch: function(arr, value, getter) { var lo = 0; var hi = arr.length; while (lo + 1 < hi) { var mid = Math.floor((hi+lo)/2); var candidate = getter(arr[mid]); if (value === candidate) { lo = mid; break; } else if (value < candidate) { hi = mid; } else { lo = mid; } } if (lo < arr.length && value === getter(arr[lo])) { return arr[lo]; } return null; }, // Returns all elements of "universe" that are not in "remove". // Both arrays must be sorted. sortedFilter: function(universe, remove, getter) { var i1 = 0; var i2 = 0; var result = []; while (i1 < universe.length && i2 < remove.length) { var v1 = getter(universe[i1]); var v2 = getter(remove[i2]); if (v1 < v2) { result.push(universe[i1]); i1++; } else if (v1 === v2) { i1++; i2++; } else { i2++; } } for (var i = i1; i < universe.length; i++) { result.push(universe[i]); } return result; }, // Shows an alert box, and logs it to Google Analytics. alert: function(message) { anal.alert(message); window.alert(message); } }; }); ================================================ FILE: client/app/js/vars.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(["knockout"], function(ko){ // MVVM class for variables function Var(){ // the "self" variable enables us to refer to the OctFile context even when // we are programming within callback function contexts var self = this; // Main Bindings self.scope = ko.observable(); self.symbol = ko.observable(); self.class_name = ko.observable(); self.dimension = ko.observable(); self.value = ko.observable(); self.complex_flag = ko.observable(); // Take values from object self.take = function(values){ ko.utils.objectForEach(values, function(key, value){ self[key](value); }); }; // Type string self.typeString = ko.computed(function(){ var isScalar = (self.dimension() === "1x1"); var isComplex = (self.complex_flag()); var isChar = (self.class_name() === "char"); var isCell = (self.class_name() === "cell"); var isFn = (self.class_name() === "function_handle"); var isLogicl = (self.class_name() === "logical"); var isNumeric = (self.class_name() === "double"); var isStruct = (self.class_name() === "struct"); var isSym = (self.class_name() === "sym"); var isTF = (self.class_name() === "tf"); var isImg = (self.class_name() === "uint8"); if ( isChar ) return "(abc)"; if ( isSym ) return "$"; if ( isTF ) return "ℒ"; if ( isStruct ) return "⊡"; if ( isLogicl && isScalar ) return "¬"; if ( isLogicl ) return "["+self.dimension()+"]¬"; if ( isImg && isComplex) return "❬"+self.dimension()+"❭*"; if ( isImg && !isComplex) return "❬"+self.dimension()+"❭"; if ( isCell && isComplex) return "{"+self.dimension()+"}*"; if ( isCell && !isComplex) return "{"+self.dimension()+"}"; if ( isFn && isComplex) return "@*"; if ( isFn && !isComplex) return "@"; if (!isNumeric ) return "?"; if ( isScalar && isComplex) return "#*"; if ( isScalar && !isComplex) return "#"; if ( isComplex) return "["+self.dimension()+"]*"; if ( !isComplex) return "["+self.dimension()+"]"; }); // Type explanation self.typeExplanation = ko.computed(function(){ var isScalar = (self.dimension() === "1x1"); var isComplex = (self.complex_flag()); var isChar = (self.class_name() === "char"); var isCell = (self.class_name() === "cell"); var isFn = (self.class_name() === "function_handle"); var isLogicl = (self.class_name() === "logical"); var isNumeric = (self.class_name() === "double"); var isStruct = (self.class_name() === "struct"); var isSym = (self.class_name() === "sym"); var isTF = (self.class_name() === "tf"); var isImg = (self.class_name() === "uint8"); if ( isChar ) return "characters"; if ( isSym ) return "symbolic"; if ( isTF ) return "transfer function"; if ( isStruct ) return "struct"; if ( isLogicl ) return "logical (boolean)"; if ( isImg ) return "uint8 data (images)"; if ( isCell && isComplex) return "complex cell array"; if ( isCell && !isComplex) return "cell array"; if ( isFn && isComplex) return "complex function handle"; if ( isFn && !isComplex) return "function handle"; if (!isNumeric ) return self.class_name(); if ( isScalar && isComplex) return "complex scalar"; if ( isScalar && !isComplex) return "scalar"; if ( isComplex) return "complex matrix"; if ( !isComplex) return "matrix"; }); // Click Method self.showDetails = function(){ alert(self.symbol()+" = "+self.value()); }; // Listen on value change self.value.subscribe(function(){}); // toString method self.toString = function(){ return "[Var:"+self.symbol()+" "+self.value()+"]"; }; } Var.sorter = function(a, b){ return a.symbol() === b.symbol() ? 0 : ( a.symbol() < b.symbol() ? -1 : 1 ); }; // Expose interface return Var; }); ================================================ FILE: client/app/js/ws-shared.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ define(["js/client", "js/ot-handler", "js/polyfill"], function(OctMethods, OtHandler){ var documentClients = {}; var socketListeners = { subscribe: function(socket) { socket.on("ws.command", socketListeners.command); socket.on("ws.save", socketListeners.save); socket.on("ws.promptid", socketListeners.promptid); socket.on("ws.doc", socketListeners.doc); socket.on("ws.rename", socketListeners.renamed); socket.on("ws.delete", socketListeners.deleted); }, command: function(cmd){ OctMethods.console.command(cmd, true); }, save: function(data){ var octFile = OctMethods.ko.viewModel.getOctFileFromName(data.filename); if (!octFile) OctMethods.editor.add(data.filename, data.content); else octFile.savedContent(data.content); }, promptid: function(promptId){ if (!promptId) return; console.log("OT PROMPT:", promptId); var otClient = OtHandler.create(promptId); otClient.attachEditor(OctMethods.prompt.instance); }, doc: function(obj){ if (!obj) return; console.log("OT SCRIPT:", obj.docId, obj.filename); var octFile = OctMethods.ko.viewModel.getOctFileFromName(obj.filename); if (!octFile) { // TODO: What to do in this case? console.log("Could not find OctFile:", obj.filename); return; } var otClient = OtHandler.create(obj.docId, octFile.content); documentClients[obj.filename] = otClient; }, renamed: function(obj){ var oldname = obj.oldname; var newname = obj.newname; var newDocId = obj.newDocId; if (!documentClients[oldname]) return; if (documentClients[newname]) return; var otClient = documentClients[oldname]; delete documentClients[oldname]; documentClients[newname] = otClient; otClient.changeDocId(newDocId); }, deleted: function(obj){ var filename = obj.filename, docId = obj.docId; if (!documentClients[filename]) return; var otClient = documentClients[filename]; delete documentClients[filename]; otClient.destroy(); OtHandler.destroy(docId); } }; function clientForFilename(filename) { return documentClients[filename]; } function forEachDocClient(cb){ for (var filename in documentClients) { if (!Object.prototype.hasOwnProperty.call(documentClients, filename)) continue; cb(filename, documentClients[filename]); } } return { listeners: socketListeners, clientForFilename: clientForFilename, forEachDocClient: forEachDocClient }; }); ================================================ FILE: client/app/main.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Main RequireJS configuration for local builds. require.config({ waitSeconds: 0, paths: { // jQuery and Plugins "jquery": "vendor/jquery/dist/jquery", "jquery.cookie": "vendor/jquery.cookie/jquery.cookie", "jquery.md5": "vendor/jquery-md5/jquery.md5", // Vendor Libraries "canvg": "vendor/canvg/dist/canvg.bundle", "knockout": "vendor/knockoutjs/dist/knockout.debug", "splittr": "vendor/splittr/splittr", "filesaver": "vendor/FileSaver/FileSaver", "canvas-toblob": "vendor/canvas-toBlob.js/canvas-toBlob", "blob": "vendor/blob/Blob", "ot": "vendor/ot/dist/ot", // NPM Libraries "SocketIOFileUpload": "../node_modules/socketio-file-upload/client", "socket.io": "../node_modules/socket.io-client/dist/socket.io", // Local Libraries "ismobile": "js/detectmobilebrowser", "base64": "js/base64v1.module", "base64-toblob": "js/base64-toBlob", "ko-takeArray": "js/ko-takeArray", "ko-flash": "js/ko-flash" }, packages: [ { name: "ace", location: "vendor/ace/lib/ace", main: "ace" } ], shim: { // jQuery Plugins "jquery.md5": ["jquery"], // CanVG "canvg": { exports: "canvg" }, // ot.js "ot": { exports: "ot" }, // Other Libraries "filesaver": { deps: ["canvas-toblob", "blob"] } } }); ================================================ FILE: client/app/privacy.txt ================================================ Privacy Policy ============== End User License Agreement (last updated January 2, 2015) ========================================================= ================================================ FILE: client/app/privacy_standalone.txt ================================================ Octave Online LLC values the privacy of our users. In order to provide excellent software, we collect and save the following information: - Scripts and other files you upload to or edit in Octave Online. - Historical transcripts of Octave commands and output, which may be associated with your account or IP address. - When using Email Sign In: your email address. - When using Google+ Sign In: your email address, name, and basic Google account information, including gender and locale. The above information is stored on Google Cloud Platform based on Council Bluffs, Iowa, USA. Additional copies of the data may be stored as backups in other physical locations and not necessarily in the Rackspace network. The information may be stored indefinitely. Deleting a file from your account on your own may not constitute full deletion from our servers. To inquire about full deletion, open a support ticket or send an email as described below. Your activity in Octave Online is recorded by Google Analytics. Google Analytics may use Google AdSense cookies in your browser to record demographics about the users of Octave Online, including, but not limited to, age and gender. Note that this information is NOT personally identifiable. You may opt out from Google Analytics by following the instructions on the following URL. https://tools.google.com/dlpage/gaoptout/ California Consumer Privacy Act (“CCPA”): Under CCPA, Californian residents have the right to declare their preferences on the sale of data for advertising and marketing purposes. If you wish to change your preferences, click this link to launch our preference portal:
We use a third-party to provide monetisation technologies for our site. You can review their privacy and cookie policy here: https://www.publift.com/privacy-policy Unless required by law, we do not give or sell personally identifiable information to third parties. If your email address changes, you may open a support ticket asking to restore access to your account. We will ask you questions about your old account to verify your identity. Octave Online LLC reserves the right to send you email announcements about Octave Online. Such announcements will be released only for major updates and reports. Octave Online LLC may also save cookies to your computer. These cookies help us keep you logged in during a session and between sessions. Disabling cookies will limit the features we are able to provide. To inquire about accessing or deleting personal data, or to ask questions about the privacy policy, you can (1) open a support ticket at https://octaveonline.uservoice.com, or (2) send an email to support@octave-online.net. ================================================ FILE: client/app/styl/all.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ @require("mixins.styl") @import "../../node_modules/normalize-styl/normalize" body { // debug-backgrounds() font: 400 14px/1.2em body_font_family color: colorfgd background-color: colorbkg } h1, h2, h3, h4, h5, h6 { font-family: heading_font_family } kbd { white-space: nowrap } .modified { font-style: italic text-decoration: underline } .divide { display: inline-block width: 0.6em } a img { border: none } .theme-header { background-color: color1 height: toolbar_height line-height: toolbar_height vertical-align: middle border: none font-size: 1.1em padding: 0 1ex white-space: nowrap overflow: hidden } .clickable { text-decoration: underline cursor: pointer } .action-link { padding: 0.5em border-radius: 0.5em color: colorfgd background-color: color2 cursor: pointer text-decoration: underline & > * { color: inherit } &:hover{ background-color: colorfg1 } &:active{ background-color: colorfg4 } } @require("flexbox.styl") @require("header.styl") @require("output_panel.styl") @require("editor.styl") @require("callouts.styl") @require("modals.styl") @require("print.styl") ================================================ FILE: client/app/styl/callouts.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ @require("mixins.styl") callout_color = #369AFF callout(shadowoffset, direction="up"){ position: absolute background-color: colorbkg border: 3px solid callout_color border-radius: 5px box-shadow: 0px shadowoffset 16px 0px rgba(0, 0, 0, 0.75) // Arrow &::before{ content: " " position: absolute width: 0 height: 0 border: 10px solid if direction is "none" { border-color: transparent } else if direction is "up" { border-color: transparent callout_color callout_color transparent } else if direction is "left" { border-color: transparent transparent callout_color callout_color } else { border-color: callout_color transparent transparent callout_color } } } #instructor-promo { callout(0px, none) top: 10px left: 10px width: 260px height: auto padding: 5px text-align: center z-index: 10 // Text Flow & > span { display: block line-height: 1em &.l1 { font-size: 1.3em } &.l2 { margin-top: 5px font-size: 1em } &.l3 { font-size: 1em margin-top: 10px cursor: pointer text-decoration: underline } } @media (max-width: responsive_width_three) { display: none } } #type_here { callout(0px, down) bottom: 25px left: 10px height: auto width: 330px max-width: 80% padding: 10px z-index: 10 // Arrow &::before { left: 10px bottom: -20px } // Text Flow & > span { display: block line-height: 1em &.l1{ font-size: 2em } &.l2{ margin-top: 5px font-size: 1.2em kbd { color: green } } &.l3{ margin-top: 10px font-size: 1.2em color: #999 } a{ color: inherit } } } #login-promo, #bucket-promo { callout(4px, up) display: block top: header_height + 15px right: 10px width: 200px height: auto text-align: center z-index: 204 padding: 10px 10px // Arrow &::before { right: 10px top: -20px } // Text Flow &#login-promo > span { display: block line-height: 1em &.l1{ font-size: 2em } // &.l2{ // font-size: 3em // } &.l3{ font-size: 1.3em margin-top: 10px } &.l4{ font-size: 1em margin-top: 10px cursor: pointer text-decoration: underline } } &#bucket-promo > span { display: block line-height: 1em &.l1{ font-size: 1.25em } &.l2{ font-size: 1em margin-top: 10px cursor: pointer text-decoration: underline } } @media (max-width: responsive_width_one) { display: none } } #share-promo{ callout(4px, up) display: block top: header_height + 15px right: 10px width: 220px height: auto text-align: center z-index: 204 padding: 10px 10px // Arrow &::before { right: 10px top: -20px } // Text Flow & > span { display: block line-height: 1.2em &.l0{ font-size: 1.4em } &.l1{ font-size: 1.3em margin-top: 4px } // &.l2{ // font-size: 1.4em // margin-top: 4px // } &.l3{ font-size: 1em margin-top: 10px cursor: pointer text-decoration: underline } } } #sync-promo{ callout(4px, left) top: header_height + toolbar_height*2 + 20px left: 60px // left position is set via JavaScript; this is a default width: 130px height: auto text-align: center z-index: 203 padding: 10px 10px // Arrow &::before { left: 10px top: -20px } // Text Flow & > span { display: block line-height: 1em &.l1{ font-size: 1.3em } &.l2{ font-size: 1.1em margin-top: 10px } &.l3{ font-size: 1em margin-top: 10px cursor: pointer text-decoration: underline } } } #create-bucket-promo{ callout(4px, left) top: header_height + toolbar_height*2 + 20px left: 150px // left position is set via JavaScript; this is a default width: 160px height: auto text-align: center z-index: 202 padding: 10px 10px // Arrow &::before { left: 10px top: -20px } // Text Flow & > span { display: block line-height: 1em &.l1{ font-size: 1.3em } &.l2{ font-size: 1.1em margin-top: 10px } &.l3{ font-size: 1em margin-top: 10px cursor: pointer text-decoration: underline } } } ================================================ FILE: client/app/styl/editor.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ @require("mixins.styl") lower-toolbar() { background-color: color2 height: toolbar_height line-height: toolbar_height vertical-align: middle white-space: nowrap overflow: hidden text-align: center } top-right-control() { cursor: pointer display: block position: absolute top: 0 right: 0 padding: 0 5px background-color: color2 } #files_container { #files_toolbar { #files_toolbar_create { flaticon-button(new, toolbar_height - 6px) top-right-control() height: toolbar_height } } #files_toolbar_lower { lower-toolbar() #files_toolbar_upload { flaticon-button(upload, toolbar_height - 6px) } #files_toolbar_refresh { flaticon-button(reload, toolbar_height - 6px) } #files_toolbar_info { flaticon-button(timemachine, toolbar_height - 6px) } } #files_list_container { position: absolute left: 0 right: 0 bottom: 0 top: 2*toolbar_height border-top: 2px solid color3 background-color: color2 overflow: auto -ms-overflow-style: -ms-autohiding-scrollbar &.is_bucket { top: toolbar_height } ul { list-style-type: none margin: 0 padding-left: 0 li { cursor: pointer padding-left: 2px line-height: 1.5em white-space: nowrap overflow: hidden &.file_active { background-color: color3 color: colorbkg .dirpart { color: mix(colorbkg, color3, 35%) } } .dirpart { color: mix(colorfgd, color2, 35%) padding-right: 1px letter-spacing: -1px } .filepart { } } } #files_drop_notification { display: block; padding: 10px 10px 0; text-align: center; } &.drag-over { &, & *{ background-color: colorerr !important color: black !important } } } } #open_container { #editor_toolbar { #editor_hamburger { hamburger-button(colorfgd) cursor: pointer display: inline-block position: relative top: 2px width: 16px height: 1em margin-right: 2px } #editor_filename {} #editor_runit { top-right-control() } } #editor_btn_container { lower-toolbar() #editor_rename { flaticon-button(edit, toolbar_height - 6px) } #editor_save { flaticon-button(save, toolbar_height - 6px) } #editor_print { flaticon-button(print, toolbar_height - 6px) } #editor_download { flaticon-button(download, toolbar_height - 6px) } #editor_delete { flaticon-button(trash, toolbar_height - 6px) } #editor_share { flaticon-button(share, toolbar_height - 6px) } #wrap_checkbox { flaticon-button(wrap, toolbar_height - 6px) & > input { display: none } } & > * { margin: 1px 2px } } #editor { position: absolute left: 0 right: 0 bottom: 0 border-top: 2px solid color3 &.taller { top: toolbar_height } &.shorter { top: toolbar_height*2 } } .editor_nofile { position: absolute left: 0 right: 0 bottom: 0 overflow: auto padding: 5px &.fullheight { top: 0 } &.taller { top: toolbar_height border-top: 2px solid color3 } &.shorter { top: toolbar_height*2 border-top: 2px solid color3 } h2{ margin: 0 font-size: 1.5em line-height: 1.5em } dl{ dt{ font-family: $code_font_family display: inline-block padding: 2px 3px 0px border-width: 1px border-style: dotted } dd::before{ font-family: $unicode_font_family content: "\2192" } dd{ margin: 2px 0px 5px 2px } } } } ================================================ FILE: client/app/styl/flexbox.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ @require("mixins.styl") #abox { // Actual sizes overridden in config.ads.abox_html position: absolute display: none top: 0 right: 0 width: 0 height: 0 background-color: color2 text-align: center .abox_content { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } #abox_default { // Width should be set via media query in config.ads.abox_html height: auto; font-size: 20px line-height: 25px padding: 5px box-sizing: border-box cursor: pointer } } #main { // Actual sizes overridden in config.ads.abox_html position: absolute display: block top: 0 right: 0 bottom: 0 left: 0 } #flexbox { display: flex flex-flow: column nowrap position: absolute top: 0 right: 0 bottom: 0 left: 0 & > header { flex: 0 0 header_height } & > section { flex: 1 1 1px display: flex flex-flow: row nowrap padding-top: gutter_size padding-bottom: gutter_size background-color: color3 & > #files_container { flex: 2 4 100px margin-right: gutter_size background-color: colorbkg } & > #open_container { flex: 6 4 400px margin-right: gutter_size background-color: colorbkg } & > #output_panel { flex: 12 4 400px overflow: hidden display: flex flex-flow: column nowrap & > #plot_container { flex: 1 1 300px overflow: hidden margin-bottom: gutter_size display: flex #plot_figure_container{ flex: 2 1 80% overflow: hidden margin-right: gutter_size background-color: colorbkg } #plot_controls_container{ flex: 1 1 20% overflow: auto -ms-overflow-style: -ms-autohiding-scrollbar background-color: colorbkg } } & > #vars_console_container { flex: 4 1 300px overflow: hidden display: flex flex-flow: row nowrap & > #vars_panel { flex: 1 1 75px min-width: 0 margin-right: gutter_size background-color: color2 position: relative @media (max-width: responsive_width_two) { display: none } } & > #console_container { flex: 8 1 325px overflow: hidden display: flex flex-flow: column nowrap position: relative & > #console_output_container { flex: 1 1 auto position: relative margin-bottom: 2px } & > #console_prompt_container { flex: 0 0 auto position: relative margin-right: 2px // Leave some space at the bottom to accommodate browser tooltips margin-bottom: 10px } } } } & > #main_menu { flex: 0 0 220px margin-left: gutter_size background-color: colorbkg } & > * { position: relative } } } .handle { position: absolute width: 15px height: 20% top: (50% - @height/2) left: 0 background-color: color3 cursor: col-resize z-index: 150 &::after { content: "\3008 \3009" color: colorbkg line-height: 20px position: absolute top: s('calc(%s - %s)', 50%, 10px) left: -8px } } ================================================ FILE: client/app/styl/hamburger.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ /*! * Based on Hamburgers * @description Tasty CSS-animated hamburgers * @author Jonathan Suh @jonsuh * @site https://jonsuh.com/hamburgers * @link https://github.com/jonsuh/hamburgers */ @require("mixins.styl") .hamburger { display: inline-block transition-property: opacity, filter transition-duration: 0.15s transition-timing-function: linear &:hover { opacity: 0.7 } .hamburger-box { width: 40px height: 24px position: relative } .hamburger-inner { top: 50% margin-top: -2px } .hamburger-inner, .hamburger-inner::before, .hamburger-inner::after { width: 40px height: 4px background-color: colorfgd border-radius: 4px position: absolute transition-property: transform transition-duration: 0.15s transition-timing-function: ease } .hamburger-inner::before, .hamburger-inner::after { content: "" display: block } .hamburger-inner { top: 0 } .hamburger-inner::before { top: 10px transition-property: transform, opacity transition-timing-function: ease transition-duration: 0.2s } .hamburger-inner::after { top: 20px } &.is-active .hamburger-inner { transform: translate3d(0, 10px, 0) rotate(45deg) } &.is-active .hamburger-inner::before { transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0) opacity: 0 } &.is-active .hamburger-inner::after { transform: translate3d(0, -20px, 0) rotate(-90deg) } } ================================================ FILE: client/app/styl/header.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ @require("mixins.styl") @require("hamburger.styl") header { background-color: color1 height: header_height position: relative #extra_header_text { &.bucket, &.project { cursor: pointer; text-decoration: underline; } } #hamburger { position: absolute cursor: pointer display: block top: 11px right: 9px .hamburger-memo { display: inline-block position: absolute top: 1px right: 44px font-family: narrow_font_family font-weight: bold font-size: 20px line-height: @font-size @media (max-width: responsive_width_one) { display: none } } } h1 { margin: 0 float: left img { display: inline-block padding-top: 2px padding-left: 3px height: 36px } } small { display: inline line-height: header_height vertical-align: middle padding-left: 5px font-size: 20px font-family: narrow_font_family } } #main_menu_content { position: absolute top: 0 right: 0 bottom: 0 left: 0 padding: 5px overflow: auto -ms-overflow-style: -ms-autohiding-scrollbar .login_btn{ cursor: pointer display: block margin: 5px auto width: 200px height: 40px } #userbox { flaticon(user) height: 50px padding-left: 60px padding-top: 10px font-size: 1.2em background-repeat: no-repeat background-position: left center a { color: inherit } } .share-url-box { text-align: center & > * { display: block } input { width: 90% margin: 1ex auto } } .instructor-programs { margin-bottom: 1ex border-bottom: 1px solid color3 ul { padding-left: 1em text-indent: -1em list-style-type: none } a { color: colorfg1 text-decoration: underline cursor: pointer } } .i18n-language-selector { text-align: center font-size: 1.2em margin: 0 auto 7px .flaticon-i18n { flaticon(i18n) background-repeat: no-repeat background-position: center center background-size: 24px 24px display: inline-block width: 24px height: 24px vertical-align: bottom } } .preference-inline-item { text-align: center font-size: 1.0em margin: 1ex auto label { cursor: pointer } } .site-control-item{ @extend .action-link position: relative display: block width: 190px font-size: 1.2em margin: 5px auto text-align: center & > * { text-decoration: underline } & > a { display: block cursor: pointer width: 100% height: 100% } } .all-buckets { strong { display: block margin-top: 8px border-bottom: 2px solid colorfg2 padding-left: 4px font-size: 1.2em } ul { margin: 0 0 3px 0 padding-left: 0 list-style-type: none } li { clear: both border-bottom: 1px solid colorfg2 line-height: 1.2em } .bucket-name { color: colorfg1 font-size: 1.2em font-family: narrow_font_family } .bucket-time { font-size: 0.9em } .bucket-delete { float: right height: 2.4em line-height: 2.4em vertical-align: middle margin-right: 3px cursor: pointer } .bucket-icon-bucket, .bucket-icon-project { float: left width: 1.2em height: 2.4em margin-right: 0.5em background-repeat: no-repeat background-position: left center } .bucket-icon-bucket { flaticon(share) } .bucket-icon-project { flaticon(project) } } #footnotes { margin-top: 20px font-size: 0.8em font-style: italic color: colorfg1 & > * { color: inherit } } } ================================================ FILE: client/app/styl/mixins.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Variables header_height = 40px gutter_size = 5px console_font_size = 14px prompt_font_size = 18px prompt_height = 28px prompt_sign_width = 20px toolbar_height = 24px ad_height = 90px ad_width = 120px responsive_width_one = 480px responsive_width_two = 700px responsive_width_three = 800px responsive_width_four = 1200px // Fonts body_font_family = "Trebuchet MS", "Arial", sans-serif code_font_family = "DejaVu Sans Mono", monospace unicode_font_family = "Arial Unicode", "Lucida Sans Unicode", "DejaVu Sans", "GNU Unifont", sans-serif heading_font_family = "Trebuchet MS", "Impact", sans-serif narrow_font_family = "Arial Narrow", "Abadi MT Condensed Light", "Helvetica CY", sans-serif; // Flaticons flaticon(name) { background-image: embedurl("../images/flaticons/"+icon_name+"/"+name+".svg") } flaticon-button(name, dim=16px) { flaticon(name) background-repeat: no-repeat background-position: center center background-size: dim dim width: dim+2px height: dim+4px display: inline-block cursor: pointer } // Utilities absolute-full-size() { position: absolute top: 0 bottom: 0 width: 100% height: 100% } debug-backgrounds() { background-color: #FFF !important & > * { background-color: #EEE !important & > * { background-color: #DDD !important & > * { background-color: #CCC !important & > * { background-color: #BBB !important & > * { background-color: #AAA !important & > * { background-color: #999 !important & > * { background-color: #888 !important & > * { background-color: #777 !important & > * { background-color: #666 !important } } } } } } } } } } hamburger-button(color){ background-image: linear-gradient(to bottom, color 0%, color 20%, transparent 20%, transparent 40%, color 40%, color 60%, transparent 60%, transparent 80%, color 80%, color 100%); } // Transition Delay Setup .transition-duration-instant{ transition-duration: 0s } .transition-duration-fast{ transition-duration: 0.5s } .transition-duration-medium{ transition-duration: 1s } .transition-duration-slow{ transition-duration: 2s } .transition-property-bgcolor{ transition-property: background-color } ================================================ FILE: client/app/styl/modals.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ modal_content_padding = 20px modal_header_height = 30px // Popover Box popover(_width, _height, _zindex){ position: absolute top: 0 left: 0 right: 0 bottom: 0 z-index: _zindex background: rgba(128, 128, 128, .8) & > div { position: absolute top: 0% left: 0% width: 100% height: 100% & > div { position: absolute top: 10% left: 10% top: s("calc(50% - (%s / 2))", _height) left: s("calc(50% - (%s / 2))", _width) width: _width height: _height overflow: auto -ms-overflow-style: -ms-autohiding-scrollbar box-sizing: border-box padding: modal_content_padding background-color: colorbkg outline: 2px solid colorfgd h2.popover-title-bar { position: relative margin: -1*modal_content_padding -1*modal_content_padding modal_content_padding height: modal_header_height background-color: color2 font-size: 20px line-height: @height vertical-align: middle text-align: center } .closebtn { flaticon-button(cross) display: block position: absolute top: 5px right: 5px } kbd { background-color: color2 border-color: color3 } } } } // Privacy Policy #privacy { popover(90%, 90%, 310) article { white-space: pre-wrap } } // Email Token #email_token { popover(50%, 60%, 320) text-align: center font-size: 1.3em line-height: 1.4em input[type="email"] { font-size: 1.4em display: block width: 95% margin: 10px auto // valid/invalid not currently used &.invalid { background-color: #E094A5 } &.valid { background-color: #94E0D4 } } .g-recaptcha > div { margin: 10px auto } } // Email Password #email_password { popover(50%, 60%, 320) text-align: center font-size: 1.3em line-height: 1.4em input[type="email"], input[type="password"] { font-size: 1.3em display: block width: 95% margin: 10px auto } p { margin: 0 auto 1em } .g-recaptcha > div { margin: 10px auto } } // Change Password #change_password { popover(50%, 50%, 320) text-align: center font-size: 1.3em line-height: 1.4em input[type="password"] { font-size: 1.3em display: block width: 95% margin: 10px auto } p { margin: 0 auto 1em } } // Bucket/Project Info #bucket_info { popover(50%, 50%, 320) .shortlink { } .shortlink-url { color: colorfg3 background-color: color2 display: inline-block font-size: 16px padding: 5px border: 1px solid colorfg3 user-select: all } .edit-shortlink { flaticon-button(edit, 16px) height: 26px vertical-align: -7px } .action-link { display: inline-block } } // Create Bucket/Project #create_bucket { popover(80%, 80%, 310) bucket_bar_height = 40px #create-bucket-content { position: absolute top: modal_header_height left: 0 right: 0 bottom: bucket_bar_height overflow: auto p { font-size: 1.1em margin: 10px } h3 { font-size: 1.4em border-bottom: 2px solid color2 margin: 10px clear: both } .bucket-butype { position: relative text-align: center label { cursor: pointer position: relative z-index: 0 font-size: 1.2em margin: 0 0.5em input { position: relative z-index: 2 } span { position: relative background-color: color2 position: inline-block padding: 0.2em 0.4em 0.2em 1.2em margin-left: -1em z-index: 1 } input[type="radio"]:checked + span { background-color: color1 } } } .bucket-files-fieldset { width: 40% margin: 0 2% 15px padding: 1% height: 150px legend { font-weight: bold } select { display: block width: 100% height: 130px } &.left { float: left } &.right { float: right } } .bucket-file-move-btns { margin-top: 80px text-align: center } .last { clear: both margin-top: 15px border-top: 2px solid color2 padding-top: 5px } } #create-bucket-bar { position: absolute bottom: 0 left: 0 right: 0 height: bucket_bar_height background-color: color1 text-align: right border-bottom: 5px solid colorbtn #create-bucket-btn { background-color: transparent font-size: 28px padding-top: 4px margin-right: 4px border: none cursor: pointer color: colorbtn } } #create-bucket-spinner { position: absolute; top: 50%; left: 50%; height: auto; width: 20%; margin-left: -10%; margin-top: -10%; } } // Upgrade to Flavor #upgrade_to_flavor { popover(50%, 50%, 320) text-align: center font-size: 1.3em line-height: 1.4em p { margin: 0 auto 1em } } // Upgrade to Tier #upgrade_to_tier { popover(50%, 50%, 320) text-align: center font-size: 1.3em line-height: 1.4em p { margin: 0 auto 1em } .action-links a { @extend .action-link display: inline-block white-space: nowrap } .minor-action-links { font-size: 0.9em color: colorfg1 font-style: italic a { color: inherit } } .bigger { font-size: 1.5em } } // File History #file_history_box { popover(50%, 50%, 320) button { cursor: pointer } } // Onboarding Div #onboarding { popover(80%, 80%, 300) font-size: 1.3em line-height: 1.3em h2{ margin: 0 padding: 0 font-size: 2em line-height: 1em } figure{ width: 90%; margin: 10px auto; padding: 5px; border-radius: 10px; border: 1px solid black; & > img{ display: block; margin: 5px auto; max-width: 100% } } #onboarding-start{ color: colorbtn background-color: color1 background-image: linear-gradient(color2, color1, to bottom); background-image: radial-gradient(ellipse at center, color2 0%, color1 100%); display: block; margin: 10px auto; padding: 0.5em; font-size: 1.5em; cursor: pointer; border-radius: 1em; border: none; background-color: blue; &:active{ background-color: #ffa84c; background-image: linear-gradient(color2, color3, to bottom); background-image: radial-gradient(ellipse at center, color2 0%, color3 100%); } } } #announcement-box { popover(60%, 60%, 250) font-size: 1.3em line-height: 1.3em } #welcome_back { popover(60%, 60%, 240) font-size: 1.3em line-height: 1.3em button { color: colorbtn background-color: color1 cursor: pointer border: none border-radius: 1em padding: 0.5em } } // Main site loading spinner #site_loading { position: fixed z-index: 200 width: 100% height: 100% pointer-events: none #cosine-pulse { background-image: embedurl("../images/logo_collections/"+logo_collection+"/cosine.svg") background-repeat: repeat-x background-size: 2000px 100% // TODO: Change to side-relative values when browser support is better background-position-x: -3000px animation: 6s cosine-width infinite cubic-bezier(.8,0,.8,1) alternate forwards, 12s cosine-position infinite ease-in-out alternate forwards animation-delay: 6s, 0s position: absolute top: 30% left: 0 width: 100% height: 40% } @keyframes cosine-width { from { background-size: 2000px 100% } to { background-size: 150px 100% } } @keyframes cosine-position { from { background-position-x: -3000px } to { background-position-x: 4000px } } #site_loading_patience { position: relative; top: 75%; width: 50%; margin: 0 auto; min-width: 18em; text-align: center; font-size: 1.2em; line-height: 1.2em; } #site_loading_more_patience { position: relative; top: 75%; width: 50%; margin: 0 auto; min-width: 18em; text-align: center; font-size: 1.2em; line-height: 1.2em; } } ================================================ FILE: client/app/styl/output_panel.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Contains styles for the output panel, console window, prompt, plot, // and variables. @require("mixins.styl") plot-svg-styles() image { -ms-interpolation-mode: nearest-neighbor image-rendering: -webkit-optimize-contrast image-rendering: -moz-crisp-edges image-rendering: crisp-edges image-rendering: pixelated } #console_output_container { background-color: colorbkg pre#console { position: absolute top: 0 right: 0 bottom: 0 left: 0 overflow: auto -ms-overflow-style: -ms-autohiding-scrollbar text-align: left font-size: console_font_size line-height: 1.3em font-family: code_font_family padding: 1ex margin: 0 &.console-wrap{ white-space: pre-wrap } span.prompt_row{ font-weight: bold; color: colorfg1; } span.prompt_error{ color: colorerr font-style: italic; } a{ color: color3 } div.inline-plot{ display: block position: relative margin: 5px 0 @media (max-width: responsive_width_two) { // Hacky way to get an absolute aspect ratio width: 95% width: calc(100% - 2ex) height: 0 padding-top: 73% } @media (min-width: responsive_width_two) { width: 560px height: 420px } svg, .inline-plot-loading { display: block position: absolute top: 0 left: 0 width: 100% height: 100% } svg { plot-svg-styles() } .inline-plot-loading { background-image: url("../../images/spinner.svg") background-position: center center background-repeat: no-repeat background-size: 30% 30% } } } #cwd_box { display: inline-block position: absolute top: 0 left: 0 padding: 3px background-color: alpha(color3, 0.5) color: colorfgd font-style: italic } #tier_background { position: absolute top: 100px left: 50% & > div { position: absolute top: -75px left: -75px width: 150px height: 150px cursor: pointer &.root { flaticon(tier-root-sq) } &.plus { flaticon(tier-plus-sq) } &.times { flaticon(tier-times-sq) } &.exp { flaticon(tier-exp-sq) } } } } #console_prompt_container { div#runtime_controls_container{ height: prompt_height background-color: colorbkg #signal{ flaticon-button(cross, 12px) position: relative top: 5px left: 5px } #seconds_remaining_container{ position: absolute top: 5px left: 25px #seconds_remaining{ font-family: code_font_family } } #add_time_container, #payload_acknowledge_container { position: absolute top: 5px left: 200px } } div#prompt_sign { float: left font-size: prompt_font_size margin-top: 6px color: colorbkg margin-right: -5px font-family: unicode_font_family } div#prompt { font-size: prompt_font_size line-height: prompt_font_size } } #plot_opener { flaticon(chart) cursor: pointer position: absolute width: 40px height: 40px top: 5px right: 5px background-color: colorbkg background-repeat: no-repeat background-position: left center } #agpl_icon { position: absolute top: 5px right: 5px width: 120px } #flavor_upgrade_div { position: absolute right: 8px bottom: 8px text-align: right font-size: 12px #upgrade_btn { display: inline-block padding: 5px 10px border-radius: 11px border: 1px solid colorfgd box-shadow: 0px 0px 5px color3 vertical-align: middle background-color: colorfg2 color: colorfgd // border: none cursor: pointer .disk { &::before { content: "⦾" } &.available { &::before { content: "⦿" } } font-size: 1.2em } } #flavor_status { display: inline-block padding: 3px 5px background-color: color2 } } #plot_container { position: relative #plot_figure_container{ position: relative #plot_svg_container, svg { absolute-full-size() } svg { plot-svg-styles() } #plot_loading { position: absolute top: 50% left: 50% width: 0 height: 0 & > img { position: absolute top: -50px left: -50px width: 100px height: 100px } } &.fullscreen { position: fixed top: header_height + gutter_size right: 0 bottom: 0 left: 0 margin: 0 !important z-index: 900 } // for rendering downloadable bitmaps #plot_canvas_container { position: absolute width: 0 height: 0 overflow: hidden } } #plot_controls_container { text-align: center .plot-nav-btn { font-size: 3em cursor: pointer &.disabled { visibility: hidden } } #plot_png_download_btn { flaticon-button(download-png, 40px) } #plot_svg_download_btn { flaticon-button(download-svg, 40px) } } } // Push down the content of the console to the bottom of the screen #console::before { content: "\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A\000A"; } #vars_panel { .theme-header { text-align: center } #vars_content { position: absolute top: toolbar_height right: 0 bottom: 0 left: 0 font-size: 1.2em line-height: 1.2em overflow: auto -ms-overflow-style: -ms-autohiding-scrollbar ul { list-style-type: none padding-left: 0 margin: 0 li { cursor: pointer padding: 0.1em white-space: nowrap border: none .vars_type { color: colorfg3 } &.ko-flash { background-color: color2lt } } } } } ================================================ FILE: client/app/styl/print.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Print stylesheet for Octave Online @media print{ // Remove all absolute positioning // and hide everything by default * { position: static !important; display: none; } // Show the essential container elements html, body, header, #flexbox, #flexbox section { display: block; } // Octave Online title bar h1 { display: block; * { display: inline; position: relative !important; } } // Console #flexbox section { background-color: white !important; } #output_panel, #vars_console_container, #console_container, #console_output_container, #console { display: inline !important; } #console { background: none !important; } #console::before { content: "\000A" !important; } #console * { display: inline !important; } // Plot #plot_container, #plot_figure_container, #plot_svg_container { display: inline !important; } #plot_container { box-shadow: none !important; } #plot_svg_container * { display: inline !important; } } ================================================ FILE: client/app/styl/themes/official/fire.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #FF4B33 color2 = #FF715E color3 = #5C0B00 colorbkg = #FFFFFF colorfgd = #000000 colorerr = #918500 colorfg1 = color1 // high contrast with colorbkg colorfg2 = color2 // medium alternate color colorfg3 = color3 // high contrast with color2 colorfg4 = color3 // darker color, for pressed button color2lt = #FFC9C2 // for changed variables colorbtn = #FFFFFF // for the text on buttons with background-color color1 icon_name = fire logo_collection = official @require("../../all.styl") // Flashing Style .ko-flash{ // background-color: lighten(color2, 20%); } .patience{ background-color: rgba(255, 255, 255, 0.8); } ================================================ FILE: client/app/styl/themes/official/ice.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #1F71FF color2 = #6BA1FF color3 = #154596 colorbkg = #FFFFFF colorfgd = #000000 colorerr = #918500 colorfg1 = color1 // high contrast with colorbkg colorfg2 = color2 // medium alternate color colorfg3 = color3 // high contrast with color2 colorfg4 = color3 // darker color, for pressed button color2lt = #ADCBFF // for changed variables colorbtn = #FFFFFF // for the text on buttons with background-color color1 icon_name = fire logo_collection = official @require("../../all.styl") // Flashing Style .ko-flash{ // background-color: lighten(color2, 20%); } .patience{ background-color: rgba(255, 255, 255, 0.8); } ================================================ FILE: client/app/styl/themes/official/lava.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #2E2422 color2 = #46322C color3 = #64443A colorbkg = #1c1c1c // matches Ace editor colorfgd = #E6D2CC colorerr = #C2C484 colorfg1 = #965341 // high contrast with colorbkg colorfg2 = color3 // high contrast with colorbkg colorfg3 = colorfgd // high contrast with color2 colorfg4 = color2 // darker color, for pressed button color2lt = #594D4B // for changed variables colorbtn = colorfg1 // for the text on buttons with background-color color1 icon_name = lava logo_collection = official @require("../../all.styl") // Flashing Style .ko-flash{ // background-color: lighten(color2, 20%) } .patience{ background-color: rgba(0, 0, 0, 0.8) } ================================================ FILE: client/app/styl/themes/official/sun.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #FFB433 color2 = #FFC45E color3 = #5C3A00 colorbkg = #FFFFFF colorfgd = #000000 colorerr = #918500 colorfg1 = color1 // high contrast with colorbkg colorfg2 = color2 // medium alternate color colorfg3 = color3 // high contrast with color2 colorfg4 = color3 // darker color, for pressed button color2lt = #FFE9C2 // for changed variables colorbtn = colorfgd // for the text on buttons with background-color color1 icon_name = fire logo_collection = official @require("../../all.styl") // Flashing Style .ko-flash{ // background-color: lighten(color2, 20%); } .patience{ background-color: rgba(255, 255, 255, 0.8); } ================================================ FILE: client/app/styl/themes/server/fire.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #AD928E color2 = #C7B5B5 color3 = #7D6363 colorbkg = #FFFFFF colorfgd = #000000 colorerr = #918500 colorfg1 = color1 // high contrast with colorbkg colorfg2 = color2 // medium alternate color colorfg3 = color3 // high contrast with color2 colorfg4 = color3 // darker color, for pressed button color2lt = #E6E6E6 // for changed variables colorbtn = #FFFFFF // for the text on buttons with background-color color1 icon_name = fire logo_collection = server @require("../../all.styl") .patience{ background-color: rgba(255, 255, 255, 0.8); } ================================================ FILE: client/app/styl/themes/server/ice.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #9090E0 color2 = #B0B0E0 color3 = #606090 colorbkg = #FFFFFF colorfgd = #000000 colorerr = #918500 colorfg1 = color1 // high contrast with colorbkg colorfg2 = color2 // medium alternate color colorfg3 = color3 // high contrast with color2 colorfg4 = color3 // darker color, for pressed button color2lt = #DEE0FF // for changed variables colorbtn = #FFFFFF // for the text on buttons with background-color color1 icon_name = fire logo_collection = server @require("../../all.styl") .patience{ background-color: rgba(255, 255, 255, 0.8); } ================================================ FILE: client/app/styl/themes/server/lava.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #292929 color2 = #303030 color3 = #494949 colorbkg = #1c1c1c // matches Ace editor colorfgd = #E6D2CC colorerr = #C2C484 colorfg1 = #757575 // high contrast with colorbkg colorfg2 = color3 // medium alternate color colorfg3 = colorfgd // high contrast with color2 colorfg4 = color2 // darker color, for pressed button color2lt = #575757 // for changed variables colorbtn = colorfgd // for the text on buttons with background-color color1 icon_name = lava logo_collection = server @require("../../all.styl") .patience{ background-color: rgba(0, 0, 0, 0.8) } ================================================ FILE: client/app/styl/themes/server/sun.styl ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ color1 = #CCBDA3 color2 = #E6D9C3 color3 = #5C5445 colorbkg = #FFFFFF colorfgd = #000000 colorerr = #918500 colorfg1 = color1 // high contrast with colorbkg colorfg2 = color2 // medium alternate color colorfg3 = color3 // high contrast with color2 colorfg4 = color3 // darker color, for pressed button color2lt = #FFFAF2 // for changed variables colorbtn = colorfgd // for the text on buttons with background-color color1 icon_name = fire logo_collection = server @require("../../all.styl") .patience{ background-color: rgba(255, 255, 255, 0.8); } ================================================ FILE: client/bower.json ================================================ { "name": "octave-online", "version": "1.0.0", "dependencies": { "requirejs": "latest", "jquery": "#^1", "knockoutjs": "latest", "jquery.cookie": "latest", "jquery-md5": "latest", "canvg": "latest", "ace": "https://github.com/vote539/ace/archive/oo.zip", "splittr": "latest", "canvas-toBlob.js": "latest", "blob": "latest", "FileSaver": "1.3.2", "ot": "latest" } } ================================================ FILE: client/package.json ================================================ { "name": "@oo/client", "version": "0.0.0", "description": "Web client front end for Octave Online", "main": "app/main.js", "scripts": { "grunt": "grunt", "bower": "bower", "stylus-watch": "npm run stylus-watch:server", "stylus-watch:server": "stylus --watch -o app/css/themes app/styl/themes/server" }, "author": "Octave Online LLC", "license": "AGPL-3.0", "engines": { "node": "18.x" }, "devDependencies": { "@oo/shared": "file:../shared", "grunt": "^1.4.1", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-requirejs": "^1.0.0", "grunt-contrib-stylus": "^1.2.0", "grunt-contrib-uglify": "^4.0.1", "grunt-contrib-watch": "^1.1.0", "grunt-regex-replace": "^0.4.0", "grunt-sync": "^0.8.2", "normalize-styl": "^8.0.1" }, "dependencies": { "bower": "^1.8.12", "grunt-cli": "^1.4.3", "socket.io-client": "^2.4.0", "socketio-file-upload": "^0.4" } } ================================================ FILE: config.sample.hjson ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ########## # NOTICE # ########## # # For additional information about these settings, refer to # config_defaults.json # # # # # # # # # # # # # # # Common configurations # # # # # # # # # # # # # # redis: { hostname: localhost port: 6379 options: { auth_pass: xxxxxxxxx } } mongo: { hostname: localhost port: 27019 db: oo } email: { provider: mailgun mailgun: { api_key: xxxxxxxxx domain: localhost } postmark: { serverToken: xxxx-xxxx-xxxx templateAlias: xxxxxxxxx } } recaptcha: { siteKey: xxxxxxxxx secretKey: xxxxxxxxx } gcp: { artifacts_bucket: artifacts.PROJECT_ID.appspot.com } # # # # # # # # # # # # # # # # # Back Server configurations # # # # # # # # # # # # # # # # # worker: { logDir: /tmp/oo_logs } session: { implementation: docker } git: { hostname: localhost } # # # # # # # # # # # # # # # # # Front Server configurations # # # # # # # # # # # # # # # # # auth: { google: { oauth_key: xxxxxxxxx.apps.googleusercontent.com oauth_secret: xxxxxxxxx } easy: { ### Make up a long random string! ### secret: xxxxxxxxx } utils_admin: { users: { ### Make up a long random string! ### admin: xxxxxxxxx } } } front: { protocol: http hostname: localhost port: 8080 listen_port: 8080 # Use "client/app" for debugging static_path: client/dist cookie: { ### Make up a long random string! ### secret: xxxxxxxxx } } # # # # # # # # # # # # # # Client configurations # # # # # # # # # # # # # # client: { gtagid: xxxxxxxxx } ================================================ FILE: config_defaults.hjson ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ############# # IMPORTANT # ############# # # This file contains default configurations for Octave Online. # # Do NOT edit this file directly, unless you are making a contribution to # Octave Online Server and wish to change these defaults for all users. # # To make custom local configurations, create a file "config.hjson" in this # directory and add the keys you want to override into that file. You can use # "config.sample.hjson" as a template for "config.hjson". # # # # # # # # # # # # # # # Common configurations # # # # # # # # # # # # # # # Redis connection info # To disable Redis (for testing only), set the hostname to "" redis: { hostname: redis port: 6379 options: { # Add a password here if your instance requires authentication: # auth_pass: xxxxxxxxx } # RATE LIMITING # Most keys added to Redis will have an expiration and an interval to touch the expiration. Setting these too low may overload Redis with expiration touch requests. # maxPayload determines when a message is too big to be sent over the main pub-sub pipe; a special client will be opened to receive the message. This makes that message slightly slower but clears up the pipe for other messages. This is most often used when transmitting large amounts of data like plot images. expire: { interval: 5000 timeout: 16000 timeoutShort: 6000 } maxPayload: 10000 } # MongoDB connection info # To disable MongoDB (for testing only), set the hostname to "" mongo: { hostname: mongod port: 27017 db: oo } # Email SaaS settings email: { # The email provider should be either "mailgun" or "postmark". provider: mailgun # Default headers for sending email from: Octave Online Server # productName and supportUrl are used only for postmark productName: Octave Online Server supportUrl: http://localhost/ } # Mailgun connection info mailgun: { api_key: xxxxxxxxx domain: localhost } # Postmark connection info postmark: { serverToken: xxxx-xxxx-xxxx # Postmark is configured to send email using a template. Please create a template in Postmark with the fields product_name, token_string, action_url, and support_url. templateAlias: xxxxxxxxx # MessageStream and Template to use for on-demand file snapshot emails. The template should have fields product_name, archive_desc, action_url, and support_url. onDemandSnapshots: { stream: "on-demand-file-snapshots", template: "on-demand-file-snapshots", } } # ReCAPTCHA connection info recaptcha: { siteKey: xxxxxxxxx secretKey: xxxxxxxxx } # Patreon info patreon: { client_id: xxxxxxxxx client_secret: xxxxxxxxx redirect_url: http://localhost/auth/patreon/callback # Send webhooks to /auth/patreon/webhook webhook_secret: xxxxxxxxx # A secret used for encoding the state to authenticate OAuth requests (make up a custom string) state_secret: xxxxxxxxx state_max_token_age: 1800000 # Where to redirect the user if they link Patreon and aren't already a member login_redirect: https://www.patreon.com/bePatron?u=xxxxxxxxx&redirect_uri=http%3A%2F%2Flocalhost%2Fauth%2Fpatreon # A mapping from Patreon tiers to Octave Online Server tiers. Multiple Patreon tiers can map to the same Octave Online tier. # Four tier names are supported with icon assets: "root", "plus", "times", and "exp". # Example: Patreon tier with ID "12345" has vanity name "plus" and maps to the Octave Online "vip" tier. tiers: { # "12345": { # name: plus # oo_tier: vip # } } } # Rackspace connection info rackspace: { username: xxxxxxxxx api_key: xxxxxxxxx identity_base_url: https://identity.api.rackspacecloud.com/v2.0/ # Fill in the following with the region as xxx and the tenant ID as yyy servers_base_url: https://xxx.servers.api.rackspacecloud.com/v2/yyy/ # The personality filename to send metadata to the instance personality_filename: /etc/oo-personality.json } # GCP connection info gcp: { # Credentials for service worker to access GCP resources. Take the JSON blob that you download from the GCP cloud console and paste it here: # credentials: { # "type": "service_account", # "project_id": "...", # "private_key_id": "...", # "private_key": "...", # ... # } credentials: false # Zone for compute API calls zone: us-central1-f # Managed Instance Group (MIG) autoscaler configurations instance_group_name: xxxxxxxxx instance_group_removal_method: abandon health_check_port: 3030 # Cloud Storage Bucket for artifacts used for i18next locales; see cloudbuild.yaml in https://github.com/octave-online/oos-translations artifacts_bucket: artifacts.PROJECT_ID.appspot.com i18next_locales_tar_gz: objects/oos-translations/i18next_locales.tar.gz # Cloud Storage Bucket for user file snapshots snapshots_bucket: reposnapshots.PROJECT_ID.appspot.com # Cloud Storage Bucket for user file archive archive_bucket: repoarchive.PROJECT_ID.appspot.com # Amount of time for snapshot links to be valid (default: 72 hours = 259,200 seconds) snapshots_duration: 259200000 } # StatsD connection info statsd: { hostname: localhost port: 8125 } # Gith server connection info (for git-http-backend and php-fpm) # TODO: Consider making the service ports configurable. gith: { hostname: utils-gith } # # # # # # # # # # # # # # # # # Back Server configurations # # # # # # # # # # # # # # # # # # Runtime details for the back server worker process worker: { # LOGGING # The log directory should exist and be writeable by the server process. # "token" is the name of the worker written at the top of the log file. logDir: /srv/logs token: local monitorLogs: { subdir: monitor } sessionLogs: { subdir: sessions depth: 3 } # SESSION QUEUE AGGRESSIVENESS # How frequently to check the queue for new jobs (ms). # When maxSessions is reached, don't get any more from the queue. clockInterval: { min: 1500 max: 2500 } maxSessions: 12 # clockStrategy: # - random = randomly choose a delay between clockInterval.min and clockInterval.max # - weighted = choose a shorter delay if there are fewer online sessions, and a longer delay if there are more online sessions clockStrategy: random # PERMISSIONS # uid to use for file ownership; set this to the uid of the user running Octave. uid: 1000 # SESSION HANDLING # When set to "destroy" (default), destroy sessions immediately when the client disconnects. When set to "ignore", don't destroy sessions immediately, and let them be cleaned up when they expire (see redis.expire). When set to "expireShort", set the expire timeout to config.redis.expire.timeoutShort and then ignore. onDisconnect: destroy } # Octave session settings session: { # COMMAND AND SESSION TIMEOUTS # All times are in milliseconds. # legalTime is the amount of time initially given to commands; different values for guests and signed-in users. legalTime: { guest: 5000 user: 10000 } # countdownExtraTime is how much time to add when the user clicks the "Add __ Seconds" button near the timeout. # countdownRequestTime determines when to show the "Add ___ Seconds" button, based on the amount of time left on the clock. # countdownRequestTimeBuffer is how much sooner the server should allow extra-time requests; this is in order to prevent race conditions if the client-side and server-side clocks are slightly out of sync. countdownExtraTime: 15000 countdownRequestTime: 3000 countdownRequestTimeBuffer: 1000 # timewarnTime is when to display the warning message in timewarnMessage that the session will be terminated soon. # timeoutTime is the amount of time a session is allowed to be idle before it is terminated by the server due to inactivity. # 10 min, 15 min timewarnTime: 600000 timeoutTime: 900000 timewarnMessage: 'NOTICE: Due to inactivity, your session will expire in five minutes.' # DATA SIZE LIMITS # All data sizes are in bytes. # payloadLimit is the number of bytes in command output before the command is paused; this prevents large amounts of data clogging the messaging pipeline and also reduces bandwidth for the user, possibly also preventing their browser from freezing. Different values for guests and signed-in users. payloadLimit: { guest: 5000 user: 10000 } # payloadMessageDelay is how much time to wait before allowing the user to restart the process; this is primarily to allow the buffer to flush before displaying the restart message. # payloadAcknowledgeDelay is how much time the user is given to restart the process. payloadMessageDelay: 100 payloadAcknowledgeDelay: 5000 # textFileSizeLimit is the maximum number of bytes per file to transmit for editing to the client. This is in place for the same reason as payloadLimit. textFileSizeLimit: 50000 # jsonMaxMessageLength is a hard ceiling on the amount of data that the GNU Octave process can send per message to the Octave Online session host process. This sometimes manifests itself in errors involving plot images that are oversize. jsonMaxMessageLength: 1000000 # URLREAD # For security, all HTTP requests from Octave processes (e.g., via the function urlread) are blocked by Octave Online Server by default. This whitelist determines regular expression patterns to match against domain names to allow requests to those domains to pass through. urlreadPatterns: [ ^example\.com$ ^(.*\.)?coursera\.org$ ] # urlreadMaxBytes is the maximum file size of URLs read from urlread/urlwrite. These bytes have to be piped between Node and Octave, similar to plot images but in the other direction. urlreadMaxBytes: 5000000 # OCTAVE SESSION HOST IMPLEMENTATION # Choices: "selinux", "docker", and "unsafe". # Refer to the README file for more details. implementation: unsafe } # Session pool settings sessionManager: { # How often to log the currently active sessions (ms) and check command lag time. logInterval: 60000 # How many sessions to keep in the pool: sessions without an owner that are pre-allocated so that when a user joins, a session is ready for them. poolSize: 2 # How long to wait between creating sessions (ms), for rate limiting. poolInterval: 5000 # Maximum time that a session can use to start up (ms); above this, the session will be destroyed before it reaches the pool. startupTimeLimit: 30000 # Tier-dependent override to select a different pool than the tier name. Set to false to use the tier name as the pool name. poolTier: false # Tier-dependent boost to give sessions in the queue (used on front server). queueBoostTime: 0 } # Server auto-maintenance settings # Maintenance is designed for when multiple back servers are in a cluster; one of the servers in the cluster can take itself offline and perform maintenance, like cleaning up caches or doing a system reboot. maintenance: { interval: 1800000 requestInterval: 5000 responseWaitTime: 3000 pauseDuration: 15000 maxNodesInMaintenance: 1 minNodesInCluster: 2 } # Git file server settings git: { # Hostname for the git file server; it is expected that this server exposes a Git daemon (git:// protocol) on default port 9418 and the create-repo service on default port 3003 (see back-filesystem/README.md). hostname: utils-gitd # Author as shown in commit messages. author: { name: Octave Online Server email: localhost@localhost } gitDaemonPort: 9418 createRepoPort: 3003 # Time limit for committing to Git; if the time limit is exceeded, the commit is aborted. commitTimeLimit: 30000 # Interval (in ms) for committing without the user having to request; behaves like an "auto-save" where changes to script files are persisted to the Git server. autoCommitInterval: 300000 # Base URL of the HTTP frontend for the Git file server httpUrl: http://localhost/ # Set this to false if your git version does not support "--allow-unrelated-histories" (before Git 2.9) supportsAllowUnrelatedHistories: true } # Docker implementation settings docker: { cwd: /home/oo gitdir: /srv/git cpuShares: 512 memoryShares: 256m diskQuotaKiB: 20480 images: { filesystemSuffix: files octaveSuffix: 'octave:prod' } } # SELinux implementation settings selinux: { cgroup: { # Name of the CGroup to create and use for Octave processes name: oo/octave # Configuration to mirror in /etc/cgconfig.d/oo.conf conf: ''' group oo/octave { perm { admin { uid = root; gid = root; } task { uid = oo; gid = oo; } } cpu { cpu.shares = 128; cpu.cfs_period_us = 10000; cpu.cfs_quota_us = 8000; } } ''' } prlimit: { # The amount of memory (address space) available to an Octave process. The default is 1GB. Note that the Octave runtime counts against the address space; it consumes in the ballpark of ~250MB, leaving the other 750MB to the user. addressSpace: 1000000000 } } # Tier settings # Tiers allow you to customize a number of different settings for different groups of users. For example, this could be used to implement a free tier and a paid tier, where the paid tier gets more resources. # A default tier, named _default, is always available. # The following settings are customizable by tier: # - ads.disabled # - sessionManager.poolSize # - sessionManager.poolTier # - sessionManager.queueBoostTime # - selinux.cgroup.name # - selinux.prlimit.addressSpace # - session.legalTime.user # - session.payloadLimit.user # - session.countdownExtraTime # - session.countdownRequestTime # - session.timewarnTime # - session.timeoutTime tiers: { # Default tier: no settings need to be customized. _default: {} # Maxima tier: set limits to very high values. _maxima: { sessionManager.poolSize: 1 selinux.prlimit.addressSpace: -1 # 15 min, 10 min, 5 min, 16 min, 21 min session.legalTime.user: 900000 session.countdownExtraTime: 600000 session.countdownRequestTime: 300000 session.timewarnTime: 960000 session.timeoutTime: 1260000 } # Example custom tier: a tier named "vip" that gets additional memory (4GB) # vip: { # selinux.prlimit.addressSpace: 4000000000 # } } # Flavor settings # Flavors are different server configurations where an entire custom server spins up to handle a session. The code currently handles this feature only on the Rackspace cloud. flavors: { # Example custom flavor: named "basic" and mapped to the Rackspace flavor "memory1-15" # basic: { # rackspaceFlavor: memory1-15 # # The image UUID to use when spinning up flavor servers # image_uuid: xxxx-xxxx-xxxx # # The UUID of the private network to which to add this server # network_uuid: xxxx-xxxx-xxxx # } } # Settings common to all flavors; may be overriden by custom flavors flavorCommon: { defaultClusterSize: 2 idleTime: 600000 statusInterval: 300000 blockVolume: true } # # # # # # # # # # # # # # # # # Front Server configurations # # # # # # # # # # # # # # # # # # Authentication settings auth: { google: { oauth_key: xxxxxxxxx.apps.googleusercontent.com oauth_secret: xxxxxxxxx } easy: { secret: xxxxxxxxx max_token_age: 900000 } password: { salt_rounds: 10 # Additional delay, in milliseconds, during auth requests delay: 100 } # Basic Auth users for utils-admin and utils-auth utils_admin: { users: { } } } # Front server settings front: { # SERVER HOST SETTINGS # protocol, hostname, and port are the *publicly-visible* URLs to be used for links, redirects, etc. protocol: http hostname: localhost port: 8080 # listen_port is the port where the Node process should attach. This might be different than the publicly-visible port if there is a proxy or other middleware. listen_port: 8080 # static_path is the directory to serve; should be either the "app" (debug) or "dist" (production) directory of the client project. # A relative path is relative to the top-level projects directory (the directory where this file is located). static_path: client/dist # LOCALE SETTINGS # Both of these settings may be overriden in either buildData.json or front_setup.js. See src/app.ts for details. # locales_path is the path matching i18next translation files, with "{{lng}}" standing in for the language. A relative path is relative to the top-level projects directory. locales_path: "front/locales/{{lng}}.yaml" # locales is a list of languages to load from the above directory. locales: ["en"] # COOKIE SETTINGS # For the primary Octave Online Server session ID; there may be other cookies set on the client side, like those used to dismiss onboarding boxes. # Please set a custom cookie secret! cookie: { name: oo.sid secret: xxxxxxxxx max_age: 7889231400 # (3 months) } # OTHER OPTIONS # Timeout (in ms) for logging status of flavor servers. flavor_log_interval: 60000 # Socket.io path. There may be situations where you need to change this, such as when performing server upgrades. socket_io_path: /socket.io # Whether to redirect HTTP to HTTPS in Express require_https: false # Interval (in ms) to clear the Express template view cache. This enables live changes to index.ejs and other template files in production. Set to 0 to disable clearing of the cache. view_cache_clear_interval: 120000 } # Redirect Service settings # These settings power "octav.onl" in official Octave Online. redirect: { # e.g., "octav.onl" hostname: localhost } # Operational Transformation settings # These are timeouts (in ms) that control the lifecycle of OT operations in Redis, used for shared workspaces. ot: { operation_expire: 120000 stats_interval: 60000 document_expire: { interval: 120000 timeout: 86400000 } } # # # # # # # # # # # # # # Client configurations # # # # # # # # # # # # # # # Advertisement settings ads: { # Whether to hide ads from an authenticated user. disabled: false, # HTML code for the block. # Example for AdSense: # # ''' # # ''' # # The function "oo_enableAds()" will be called when it is confirmed that the current session is not from a paid subscriber: # # ''' # # ''' head_html: ''' ''' # HTML code for the "abox" div in the tag. The div must be enabled via CSS in a # # # ''' abox_html: ''' ''' } client: { # THEME COLLECTION # Choices: "server", "official", or some other theme collection that you create manually. # To avoid confusing the end user, do not use the theme collection "official" unless you are running on octave-online.net. theme_collection: server # FRONTEND OPTIONS # Title and meta-tag description to use, based on a key in the translation file. Before being added to the translation files, these options were "title" and "description" directly in this configuration file. title_key: "header.meta.server.title" description_key: "header.meta.server.desc" # Theme color; should be consistent with color1 in fire.styl theme_color: AD928E # Brand name, same across languages. app_name: Octave Online Server # Whether to show the splash screen and other onboarding bubbles to help new users learn how to use the software. onboarding: false # Number of milliseconds since the user's last_activity before displaying the Welcome Back message. Defaults to 200 days. welcome_back_ms: 17280000000 # ANNOUNCEMENT MESSAGE # Set announcement_display to enable a dialog that is shown to users when visiting the page. There are three options: # - "on" => show the message to all visitors; this could announce expected system downtime, for example # - "returning" => show the message to returning users only (those who have dismissed the onboarding screen); this could announce new feature to existing users, for example. This setting only works if onboarding is set to true # - "off" => disable the announcement screen # TODO: Re-engineer a way to set the announcement without rebuilding the app. announcement_display: "off" # Use the following variable to control the HTML shown in the announcement box. announcement_html: '''

Example announcement

''' # OTHER CLIENT OPTIONS # Application identifier for Google Universal Analytics (deprecated) gacode: xxxxxxxxx # Google Tag ID (GA4 Measurement ID). Instructions for how to get this: # gtagid: xxxxxxxxx # Application identifier for UserVoice uservoice: xxxxxxxxx } ================================================ FILE: containers/README.md ================================================ Containers for Octave Online Server =================================== This directory contains up-to-date container definitions for various pieces of Octave Online Server. Inside most directories, you will find at least two files: - Dockerfile: The scripts to build the container. - cloudbuild.yaml: For hooking up with Google Cloud Build. You can ignore this file unless you want to use Google Cloud Build to build the containers. Some directories contain additional assets used for building the respective containers. **Important:** You should build all of the containers from the repository root and specify the Dockerfile via the `--file` option to `docker build` or `docker-compose`. If necessary, your *config.hjson* file should be present when you run `docker build`. The containers are: - octave-\*: GNU Octave containers - oo-front: Front server container - oo-back: Back server container - utils-gitd: Essential Git file server - oo-gith: Frontend for the human-friendly file history viewer - utils-gith: Backend for the human-friendly file history viewer - utils-admin: Optional administration panel ## Running with Docker Compose [Docker Compose](https://docs.docker.com/compose/) lets you configure and run multiple containers from a single configuration file. Octave Online Server ships with *containers/oos-quick-start/docker-compose.yml* to get you off the ground quickly. :bangbang: **Important:** With this installation method, Octave sessions are sandboxed from the host machine, but they are *not* sandboxed from each other or from the network! You ***should not*** use this installation method if you plan to make Octave Online Server open to untrusted users. The intended audience for the Docker Compose version of Octave Online Server are research labs and other environments where users are known and trusted. ### Installing Docker Compose 1. [Install Docker Engine](https://docs.docker.com/engine/install/) 2. [Install Docker Compose](https://docs.docker.com/compose/install/) 3. Optional: [Set up Docker with a non-root user](https://docs.docker.com/engine/install/linux-postinstall/) ### Building All Images From the repository root: ```bash $ docker-compose -f containers/oos-quick-start/docker-compose.yaml build ``` It takes approximately one hour to build from scratch. If you see an error such as "npm ERR! code ENOENT", please run the command again until it succeeds. ### Running Octave Online Server From the repository root: ```bash $ docker-compose -f containers/oos-quick-start/docker-compose.yaml run --publish 8080:8080 -d oo-front $ docker-compose -f containers/oos-quick-start/docker-compose.yaml run -d oo-back ``` Octave Online Server should now be running on port 8080. To run the file history server on port 8008: ```bash $ docker-compose -f containers/oos-quick-start/docker-compose.yaml run --publish 8008:8008 -d oo-gith ``` ### Configuration File with Docker Compose Octave Online Server should run out of the box on Docker Compose without a custom configuration file. To customize settings, create a config.hjson file as usual and rebuild the images. When creating a custom config.hjson file, do *not* overwrite the various "hostname" and "port" settings for services that run in containers. The default settings are required for the Docker containers to talk to each other. Additionally, the "unsafe" mode for session.implementation is the only option known to work when the back server is running inside a container. Examples of settings that you may want to configure: - session.legalTime.\* (amount of time allocated to running commands) - mailgun.\* (Mailgun settings for email) - auth.google.\* (Google OAuth settings) - auth.easy.secret (salt for encrypting email tokens) - front.cookie.secret (salt for encrypting session cookies) ### Optional: Create custom volumes for application data By default, Docker will create volumes under */var/lib/docker/volumes* for Octave Online Server application data. If you want to customize where application data is stored, you can create your own volumes in Docker. ```bash # Loopback device to store MongoDB data (2 GB) $ sudo dd if=/dev/zero of=/mnt/docker_mongodb.img bs=100M count=20 $ sudo mkfs.xfs /mnt/docker_mongodb.img $ sudo losetup -fP /mnt/docker_mongodb.img # Loopback device to store Git user data (4 GB) $ sudo dd if=/dev/zero of=/mnt/docker_git.img bs=100M count=40 $ sudo mkfs.xfs /mnt/docker_git.img $ sudo losetup -fP /mnt/docker_git.img # Check the loopback mount locations: $ losetup -a /dev/loop0: []: (/mnt/docker_mongodb.img) /dev/loop1: []: (/mnt/docker_git.img) # Create the volumes in Docker; set the device paths according to `losetup -a` $ docker volume create --driver local \ --opt type=xfs \ --opt device=/dev/loop0 \ oosquickstart_mongodb $ docker volume create --driver local \ --opt type=xfs \ --opt device=/dev/loop1 \ oosquickstart_git ``` A third volume, `oosquickstart_logs`, is used to save session logs (input/output on a per-session basis). If you don't wish to save that information, you can mount a tmpfs as `oosquickstart_logs`. If you do wish to save that information, you can create devices as shown above for `oosquickstart_mongodb` and `oosquickstart_git`. ## About the GNU Octave Containers There are four containers that are intended to be built in sequence, each one depending on the previous one. The order is: 1. octave-deps (install build dependencies) 2. octave-stable (build vanilla GNU Octave from source) 3. octave-pkg (build packages) 4. octave-oo (build extensions for Octave Online Server) Example commands to build these four containers in sequence (**Note: You do not need to run these commands if you are using docker-compose**) ```bash # Run these commands from the top level directory $ export SHORT_SHA=$(git rev-parse HEAD | cut -c 1-7) $ docker build \ --tag=octave-deps:$SHORT_SHA \ --file=containers/octave-deps/Dockerfile \ . $ docker build \ --build-arg=FULL_BASE_IMAGE=octave-deps:$SHORT_SHA \ --tag=octave-stable:$SHORT_SHA \ --file=containers/octave-stable/Dockerfile \ . $ docker build \ --build-arg=FULL_BASE_IMAGE=octave-stable:$SHORT_SHA \ --tag=octave-pkg:$SHORT_SHA \ --file=containers/octave-pkg/Dockerfile \ . $ docker build \ --build-arg=FULL_BASE_IMAGE=octave-pkg:$SHORT_SHA \ --tag=octave-oo:$SHORT_SHA \ --file=containers/octave-oo/Dockerfile \ . ``` There are also *cloudbuild.yaml* files in each directory in case you want to use the Google Cloud Build service to build the Docker images. With these files, you build each image in sequence, and when you get a clean build, set that image's tag as the `_BASE_REV` substitution on the subsequent image. Each tag gets built based on the previous tag, so the tag in the end will have four short SHAs in sequence: "octave-oo:aaaaaaa-bbbbbbb-ccccccc-ddddddd" ================================================ FILE: containers/octave-deps/Dockerfile ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . FROM rockylinux:8 WORKDIR /root # Development tools for Octave RUN yum groupinstall -y "Development Tools" RUN yum install -y \ cmake \ epel-release \ git \ net-tools \ librsvg2-tools \ traceroute \ wget \ yum-utils # EL 8: RUN dnf config-manager --set-enabled powertools # EL 9: # RUN dnf config-manager --set-enabled crb # Library dependencies for Octave (and other deps only in EPEL) RUN yum install -y \ arpack-devel \ bzip2-devel \ eigen3-devel \ fftw-devel \ fltk-devel \ gl2ps-devel \ glpk-devel \ gnuplot \ gperf \ GraphicsMagick-c++-devel \ hdf5-devel \ icoutils \ java-17-openjdk-devel \ lapack-devel \ libqhull \ libsndfile-devel \ llvm-devel \ mercurial \ openblas-devel \ pcre-devel \ portaudio-devel \ qhull-devel \ qrupdate-devel \ suitesparse-devel \ texinfo \ texinfo-tex \ transfig \ zlib-devel # sundials-devel is in EPEL 8: RUN yum install -y sundials-devel # sundials-devel is missing from EPEL 9, so we need to install it separately. # https://github.com/LLNL/sundials/issues/244 # RUN git clone -b v6.5.0 --depth 1 https://github.com/LLNL/sundials.git && \ # cd sundials && \ # git submodule update --init && \ # mkdir BUILDDIR && \ # cd BUILDDIR && \ # cmake -DENABLE_KLU=ON -DKLU_INCLUDE_DIR=/usr/include/suitesparse -DKLU_LIBRARY_DIR=/usr/lib64 .. && \ # time make -j8 && \ # make install # Manually install rapidjson; see comments in configure.ac RUN git clone https://github.com/Tencent/rapidjson.git && \ cd rapidjson && \ git reset --hard fd3dc29a5c2852df569e1ea81dbde2c412ac5051 && \ git submodule update --init && \ mkdir build && \ cd build && \ cmake .. && \ time make -j8 && \ make install # TODO: It's not clear which is the "correct" way to set the environment variable RUN echo "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk" > /etc/profile.d/oo.sh ENV JAVA_HOME /usr/lib/jvm/java-17-openjdk # TODO # arpack-devel # atlas-devel # bison # libcurl-devel # desktop-file-utils # fftw-devel # flex # fltk-devel # ftgl-devel # gcc-gfortran # ghostscript # gl2ps-devel # glpk-devel # gnuplot # gperf # GraphicsMagick-c++-devel # hdf5-devel # less # libX11-devel # llvm-devel # mesa-libGL-devel # mesa-libGLU-devel # ncurses-devel # pcre-devel # qhull-devel # qrupdate-devel # qscintilla-devel # readline-devel # suitesparse-devel # texinfo # texinfo-tex # zlib-devel # When building without --disable-docs, the following additional packages are required: # texlive-collection-latexrecommended # texlive-metapost ================================================ FILE: containers/octave-deps/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: - name: gcr.io/cloud-builders/docker args: - build - --tag=gcr.io/$PROJECT_ID/octave-deps:$SHORT_SHA - --file=containers/octave-deps/Dockerfile - '.' timeout: 7200s timeout: 7200s images: ['gcr.io/$PROJECT_ID/octave-deps:$SHORT_SHA'] ================================================ FILE: containers/octave-oo/Dockerfile ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ARG FULL_BASE_IMAGE FROM $FULL_BASE_IMAGE RUN yum install -y \ json-c-devel \ libuv-devel # Copy and compile host.c # Note: path is relative to repository root COPY back-octave back-octave RUN cd back-octave && make && make install # Apply patches ### 4.0.1-rc1 ### # RUN cd octave && \ # hg update 323e92c4589f && \ # hg import ../back-octave/oo-changesets/001-d38b7c534496.hg.txt && \ # hg import ../back-octave/oo-changesets/002-d3de6023e846.hg.txt && \ # hg import ../back-octave/oo-changesets/003-4d28376c34a8.hg.txt && \ # hg import ../back-octave/oo-changesets/004-6ff3e34eea77.hg.txt && \ # hg import ../back-octave/oo-changesets/005-9e73fe0d92d5.hg.txt && \ # hg import ../back-octave/oo-changesets/006-15d21ceec728.hg.txt && \ # hg import ../back-octave/oo-changesets/007-4d778d6ebbd0.hg.txt && \ # hg import ../back-octave/oo-changesets/008-e8ef7f3333bf.hg.txt && \ # hg import ../back-octave/oo-changesets/009-05f7272c001e.hg.txt && \ # hg import ../back-octave/oo-changesets/010-4a1afb661c55.hg.txt && \ # hg import ../back-octave/oo-changesets/011-7327936fa23e.hg.txt && \ # hg import ../back-octave/oo-changesets/012-84390db50239.hg.txt && \ # hg import ../back-octave/oo-changesets/013-f4110d638cdb.hg.txt && \ # hg import ../back-octave/oo-changesets/014-21fd506b7530.hg.txt ### 4.2.1 ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/100-2d1fd5fdd1d5.hg.txt && \ # hg import ../back-octave/oo-changesets/101-bc8cd93feec5.hg.txt && \ # hg import ../back-octave/oo-changesets/102-30d8ba0fbc32.hg.txt && \ # hg import ../back-octave/oo-changesets/103-352b599bc533.hg.txt && \ # hg import ../back-octave/oo-changesets/104-9475120a3110.hg.txt && \ # hg import ../back-octave/oo-changesets/105-ccbef5c9b050.hg.txt && \ # hg import ../back-octave/oo-changesets/106-91cb270ffac0.hg.txt && \ # hg import ../back-octave/oo-changesets/107-80081f9d8ff7.hg.txt && \ # hg import ../back-octave/oo-changesets/108-9b39ca8bcbfd.hg.txt ### 5.2 ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/200-84cbf166497f.hg.txt && \ # hg import ../back-octave/oo-changesets/201-b993253f19d0.hg.txt && \ # hg import ../back-octave/oo-changesets/202-d9d23f97ba78.hg.txt && \ # hg import ../back-octave/oo-changesets/203-d6b5ffb8e4cc.hg.txt && \ # hg import ../back-octave/oo-changesets/204-e61d7b8918e2.hg.txt ### 6.0.1 (based on 9e7b2625e574) ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/300-d78448f9c483.hg.txt && \ # hg import ../back-octave/oo-changesets/301-97f7d1f4fe83.hg.txt && \ # hg import ../back-octave/oo-changesets/302-8900d7cf8554.hg.txt ### 6.0.1 (based on 171a2857d6d1) ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/310-1e1c91e6cddc.hg.txt ### 6.4.0 (based on 8d7671609955) ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/320-8d4683a83238.hg.txt && \ # hg import ../back-octave/oo-changesets/321-faad58416a3a.hg.txt ### 7.0.1 (based on 117ebe363f56) ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/400-7ade2492e023.hg.txt && \ # hg import ../back-octave/oo-changesets/401-1b33dc797ec9.hg.txt && \ # hg import ../back-octave/oo-changesets/402-b01fa2864d4d.hg.txt && \ # hg import ../back-octave/oo-changesets/403-2813cb96e10f.hg.txt && \ # hg import ../back-octave/oo-changesets/404-acb523f25bb9.hg.txt && \ # hg import ../back-octave/oo-changesets/405-6ad34b0b69e1.hg.txt && \ # hg import ../back-octave/oo-changesets/406-d0df6f16f41e.hg.txt && \ # hg import ../back-octave/oo-changesets/407-df206dd11399.hg.txt && \ # hg import ../back-octave/oo-changesets/408-8184a51579f3.hg.txt ### 7.4 (based on 601e7a142a15) ### # RUN cd octave && \ # hg import ../back-octave/oo-changesets/420-4c3d80dd9e65.hg.txt && \ # hg import ../back-octave/oo-changesets/421-de16dd99ab0e.hg.txt && \ # hg import ../back-octave/oo-changesets/422-de16dd99ab0e.hg.txt ### 8.4 (based on 78c13a2594f3) ### RUN cd octave && \ hg import ../back-octave/oo-changesets/430-d2250ae9bddd.hg.patch # Re-build with patches RUN cd octave/.build && make -j8 RUN cd octave/.build && make install # Monkey-patch bug #42352 # https://savannah.gnu.org/bugs/?42352 # RUN touch /usr/local/share/octave/4.2.1/etc/macros.texi # # Monkey-patch json-c runtime errors # ENV LD_LIBRARY_PATH /usr/local/lib # Install site octaverc.m (formerly part of "install-site-m" in Makefile) COPY containers/octave-pkg/octaverc.m /usr/local/share/octave/site/m/startup/octaverc # Install the java.opts file COPY containers/octave-oo/java.opts /usr/local/share/octave/8.0.1/m/java/java.opts ================================================ FILE: containers/octave-oo/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: - name: 'gcr.io/cloud-builders/docker' args: - build - --build-arg=FULL_BASE_IMAGE=gcr.io/$PROJECT_ID/octave-pkg:$_BASE_REV - --tag=gcr.io/$PROJECT_ID/octave-oo:$_BASE_REV-$SHORT_SHA - --tag=gcr.io/$PROJECT_ID/octave-oo:latest - --file=containers/octave-oo/Dockerfile - '.' timeout: 7200s timeout: 7200s options: machineType: 'N1_HIGHCPU_8' substitutions: _BASE_REV: unknown images: - gcr.io/$PROJECT_ID/octave-oo:$_BASE_REV-$SHORT_SHA - gcr.io/$PROJECT_ID/octave-oo:latest ================================================ FILE: containers/octave-oo/java.opts ================================================ -Xms4m -Xmx32m -XX:MetaspaceSize=4m -XX:MaxMetaspaceSize=32m -XX:-TieredCompilation ================================================ FILE: containers/octave-pkg/Dockerfile ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ARG FULL_BASE_IMAGE FROM $FULL_BASE_IMAGE # Install some popular Octave Forge packages. # If a package fails to install, try building the image again and it might work the second time. # Most packages are auto-loaded via octaverc (since version 4.2.1) except for the following packages that shadow core library functions or are slow to load: tsa, stk, ltfat, and nan. # Note: The package list gets written to /usr/local/share/octave/octave_packages # rpmfusion is required for ffmpeg RUN dnf install -y --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm RUN yum install -y \ units \ mpfr-devel \ portaudio-devel \ patch \ ncurses-devel \ libicu-devel \ netcdf-devel \ nettle-devel \ gdal-devel \ python3-pip # EL 8: RUN yum install -y ffmpeg-devel RUN pip3 install sympy==1.9 # EL 9: # RUN yum install -y compat-ffmpeg4-devel; # RUN pip3 install sympy==1.11.1 ARG PKG_BASE_URL=https://downloads.sourceforge.net/project/octave/Octave%20Forge%20Packages/Individual%20Package%20Releases RUN mkdir pkg-downloads RUN cd pkg-downloads && wget https://github.com/apjanke/octave-tablicious/releases/download/v0.3.7/tablicious-0.3.7.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install tablicious-0.3.7.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/symbolic-3.1.1.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install symbolic-3.1.1.tar.gz;" RUN cd pkg-downloads && wget https://github.com/gnu-octave/pkg-control/releases/download/control-3.6.1/control-3.6.1.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install control-3.6.1.tar.gz;" # NOTE: There is an open Merge Request to fix a variables issue in signal-1.4.5 # https://sourceforge.net/p/octave/signal/merge-requests/4/ RUN cd pkg-downloads && wget $PKG_BASE_URL/signal-1.4.4.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install signal-1.4.4.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/communications-1.2.6.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install communications-1.2.6.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/struct-1.0.18.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install struct-1.0.18.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/io-2.6.4.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install io-2.6.4.tar.gz;" RUN cd pkg-downloads && wget -O statistics-1.6.0.tar.gz https://github.com/gnu-octave/statistics/archive/refs/tags/release-1.6.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install statistics-1.6.0.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/optim-1.6.2.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install optim-1.6.2.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/image-2.14.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install image-2.14.0.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/general-2.1.3.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install general-2.1.3.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/matgeom-1.2.3.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install matgeom-1.2.3.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/linear-algebra-2.2.3.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install linear-algebra-2.2.3.tar.gz;" # RUN cd pkg-downloads && wget $PKG_BASE_URL/geometry-4.0.0.tar.gz && \ # LC_ALL=C /usr/local/bin/octave -q --eval "pkg install geometry-4.0.0.tar.gz;" # NOTE: The geometry package has a compile error with modern GCC: # https://sourceforge.net/p/octave/geometry/ci/04965cda30b5f9e51774194c67879e7336df1710/ RUN cd pkg-downloads && \ hg clone -r 04965c http://hg.code.sf.net/p/octave/geometry && \ (cd geometry && hg archive geometry.tar.gz) && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install geometry/geometry.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/data-smoothing-1.3.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install data-smoothing-1.3.0.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/nan-3.7.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install nan-3.7.0.tar.gz;" # NOTE: This package is below the download threshold, but it is still receiving updates RUN cd pkg-downloads && wget $PKG_BASE_URL/tsa-4.6.3.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install tsa-4.6.3.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/miscellaneous-1.3.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install miscellaneous-1.3.0.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/interval-3.2.1.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install interval-3.2.1.tar.gz;" # NOTE: This package is below the download threshold, but it is still receiving updates RUN cd pkg-downloads && wget https://github.com/stk-kriging/stk/releases/download/2.8.1/stk-2.8.1-octpkg.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install stk-2.8.1-octpkg.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/mapping-1.4.2.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install mapping-1.4.2.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/financial-0.5.3.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install financial-0.5.3.tar.gz;" RUN cd pkg-downloads && wget https://github.com/ltfat/ltfat/releases/download/v2.6.0/ltfat-2.6.0-of.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install ltfat-2.6.0-of.tar.gz;" # Workaround for bug in package ltfat: # https://github.com/ltfat/ltfat/issues/115 #COPY containers/octave-pkg/ltfat.patch pkg-downloads/ltfat.patch #RUN cd pkg-downloads && wget $PKG_BASE_URL/ltfat-2.3.1.tar.gz && \ # tar zxf ltfat-2.3.1.tar.gz && \ # patch --ignore-whitespace -p0 < ltfat.patch && \ # tar czf ltfat_fix.tar.gz ltfat && \ # LC_ALL=C /usr/local/bin/octave -q --eval "pkg install ltfat_fix.tar.gz;" RUN cd pkg-downloads && wget https://github.com/Andy1978/octave-video/releases/download/2.1.1/video-2.1.1.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install video-2.1.1.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/netcdf-1.0.17.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install netcdf-1.0.17.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/dataframe-1.2.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install dataframe-1.2.0.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/mvn-1.1.0.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install mvn-1.1.0.tar.gz;" RUN cd pkg-downloads && wget $PKG_BASE_URL/fuzzy-logic-toolkit-0.4.6.tar.gz && \ LC_ALL=C /usr/local/bin/octave -q --eval "pkg install fuzzy-logic-toolkit-0.4.6.tar.gz;" # Policy for adding or removing packages: # - Add packages that garner at least 40 downloads/week # - Remove packages that fall below 10 downloads/week # Former packages: # - mechanics-1.3.1 (dropped due to only 4 downloads/week) # - divand-1.1.2 (dropped due to only 8 downloads/week) # Generate package metadata, used for warning messages RUN cd /usr/local/share/octave/site/m && /usr/local/bin/octave -q --eval "\ packages = {}; \ for p=pkg('list'); \ packages = {packages{:} pkg('describe', '-verbose', p{1}.name){:}}; \ endfor; \ save('package_metadata.mat', 'packages'); " ================================================ FILE: containers/octave-pkg/README.md ================================================ Packages for Octave Online Server ================================= ## Methodology There are 70+ packages on Octave Forge and many others hosted elsewhere. In order to determine the relative popularity of packages, the page [Individual Package Releases](https://sourceforge.net/projects/octave/files/Octave%20Forge%20Packages/Individual%20Package%20Releases/) is referenced. The following methodology is used to determine packages to add or remove: 1. Add packages that have at least 40 downloads/week. 2. Keep packages that have less than 40 but at least 10 downloads/week. 3. Drop packages that have less than 10 downloads/week. In addition, no packages whose primary purpose is interacting with hardware devices, since Octave Online Server does not support hardware devices. ================================================ FILE: containers/octave-pkg/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: - name: 'gcr.io/cloud-builders/docker' args: - build - --build-arg=FULL_BASE_IMAGE=gcr.io/$PROJECT_ID/octave-stable:$_BASE_REV - --tag=gcr.io/$PROJECT_ID/octave-pkg:$_BASE_REV-$SHORT_SHA - --file=containers/octave-pkg/Dockerfile - '.' timeout: 7200s timeout: 7200s substitutions: _BASE_REV: unknown images: ['gcr.io/$PROJECT_ID/octave-pkg:$_BASE_REV-$SHORT_SHA'] ================================================ FILE: containers/octave-pkg/ltfat.patch ================================================ diff -u -r ltfat/inst/nonstatgab/nsdgt.m ltfat/inst/nonstatgab/nsdgt.m --- ltfat/inst/nonstatgab/nsdgt.m 2018-06-21 15:03:11.000000000 +0000 +++ ltfat/inst/nonstatgab/nsdgt.m 2020-07-05 01:52:46.072676195 +0000 @@ -149,8 +149,8 @@ col = ceil(Lg/M(ii)); temp = zeros(col*M(ii),W,assert_classname(f,g{1})); - temp([end-floor(Lg/2)+1:end,1:ceil(Lg/2)],:) = bsxfun(@ ... - times,f(win_range,:),g{ii}(idx)); + temp([end-floor(Lg/2)+1:end,1:ceil(Lg/2)],:) = bsxfun(@times, ... + f(win_range,:),g{ii}(idx)); temp = reshape(temp,M(ii),col,W); X = squeeze(fft(sum(temp,2))); ================================================ FILE: containers/octave-pkg/octaverc.m ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . # Clear Vars Patch from wiki.octave.org function clear (varargin) args = sprintf (', "%s"', varargin{:}); # this line is kind-of funky, but without it, running "clear" # without any arguments does not work if strcmp(args, ', "') args = ''; end evalin ("caller", ['builtin ("clear"' args ')']); pkglist = pkg ("list"); loadedpkg = cell (0); for ii = 1:numel (pkglist) if (pkglist{ii}.loaded) loadedpkg{end+1} = pkglist{ii}.name; endif endfor if exist("~/.octaverc") source("~/.octaverc"); endif source("/usr/local/share/octave/site/m/startup/octaverc"); if (numel (loadedpkg) != 0) pkg ("load", loadedpkg{:}); endif endfunction % Set environment variables % 2022-03-06: Java does not have enough memory to support xlsopen, so disable it by default setenv("PYTHON", "python3"); setenv("OCTAVE_JAVA_DIR", "/dev/null"); % Auto-load packages (no more pkg-auto since 4.2.1) % Packages that are installed but not auto-loaded have a reason, such as shadows. pkg load communications; pkg load control; pkg load dataframe; pkg load fuzzy-logic-toolkit; pkg load general; pkg load image; pkg load interval; pkg load linear-algebra; pkg load miscellaneous; pkg load mvn; pkg load signal; pkg load struct; pkg load symbolic; pkg load video; % 2023-01-03: The statistics package contains known shadows in Octave 8.4. % See: https://github.com/gnu-octave/statistics/issues/121 % Remove this condition upon upgrading to Octave 9. warning("off","Octave:shadowed-function"); pkg load statistics; warning("on","Octave:shadowed-function"); % Packages not auto-loaded because of the JVM dependency or other performance tradeoff: % pkg load data-smoothing; % pkg load financial; % pkg load io; % pkg load mapping; % pkg load netcdf; % pkg load optim; % pkg load stk; % Packages not auto-loaded due to shadow functions: % pkg load geometry; % pkg load ltfat; % pkg load matgeom; % pkg load nan; % pkg load tablicious; % pkg load tsa; ================================================ FILE: containers/octave-stable/Dockerfile ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ARG FULL_BASE_IMAGE FROM $FULL_BASE_IMAGE # Enlist and Configure the correct Octave revision ### 4.2.1 ### # RUN hg clone --insecure -r b9d482dd90f3 http://www.octave.org/hg/octave ### 5.2-rc ### # RUN hg clone --insecure -r 56dd7419d7aa http://www.octave.org/hg/octave ### 6.0.1 ### # RUN hg clone --insecure -r 9e7b2625e574 http://www.octave.org/hg/octave ### 6.0.1, after fix to bug #58698 ### # RUN hg clone --insecure -r 171a2857d6d1 http://www.octave.org/hg/octave ### 6.4.0 ### # RUN hg clone --insecure -r 8d7671609955 http://www.octave.org/hg/octave ### 7.0.1 ### # RUN hg clone --insecure -r 117ebe363f56 http://www.octave.org/hg/octave ### 7.4 ### # RUN hg clone -r 601e7a142a15 https://hg.octave.org/octave ### 8.4 ### RUN hg clone --insecure -r 78c13a2594f3 http://hg.octave.org/octave RUN cd octave && \ ./bootstrap && \ mkdir .build ### 4.0.1-rc1 ### # RUN cd octave/.build && \ # ../configure --disable-readline --disable-gui --disable-docs ### 4.2.1 ### # Note: set GNUPLOT=... if you are using a custom gnuplot! # RUN cd octave/.build && \ # ../configure --disable-readline --disable-docs --disable-atomic-refcount --without-qt ### 5.2-rc ### # Note: rsvg-convert is not to be found in the CentOS repos, but it is not really necessary for a command-line build, so replace it with "echo" # RUN cd octave/.build && \ # RSVG_CONVERT=echo ICOTOOL=echo ../configure --disable-readline --disable-docs --disable-atomic-refcount --without-qt --without-opengl ### 6.4.0 ### ### 7.0.1 ### ### 7.4 ### RUN cd octave/.build && \ ICOTOOL=echo ../configure --disable-readline --disable-docs --without-qt --without-opengl # Build Octave # This is the slowest part of the Dockerfile RUN cd octave/.build && make -j8 RUN cd octave/.build && make install ================================================ FILE: containers/octave-stable/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: - name: 'gcr.io/cloud-builders/docker' args: - build - --build-arg=FULL_BASE_IMAGE=gcr.io/$PROJECT_ID/octave-deps:$_BASE_REV - --tag=gcr.io/$PROJECT_ID/octave-stable:$_BASE_REV-$SHORT_SHA - --file=containers/octave-stable/Dockerfile - '.' timeout: 7200s timeout: 7200s options: machineType: 'N1_HIGHCPU_8' substitutions: _BASE_REV: unknown images: ['gcr.io/$PROJECT_ID/octave-stable:$_BASE_REV-$SHORT_SHA'] ================================================ FILE: containers/oo-back/Dockerfile ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ARG FULL_BASE_IMAGE FROM $FULL_BASE_IMAGE # Core tmpfs directories: VOLUME \ /run \ /tmp # Install dependencies, including Node.js RUN dnf module install -y nodejs:18/common RUN dnf install -y gcc-c++ make python3 RUN npm config set prefix /workspace # Copy the application code into the container RUN mkdir -p /srv/oo/projects COPY shared /srv/oo/projects/shared COPY back-filesystem /srv/oo/projects/back-filesystem COPY back-master /srv/oo/projects/back-master COPY config*.hjson /srv/oo/projects/ # Build Node.js projects for oo-back # Use npm ci to install deps from lockfiles RUN \ cd /srv/oo/projects/shared && npm ci && \ cd /srv/oo/projects/back-filesystem && npm ci && \ cd /srv/oo/projects/back-master && npm ci CMD DEBUG=* node /srv/oo/projects/back-master/app.js ================================================ FILE: containers/oo-back/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: # Pull additional data from GCP, including config.hjson. # NOTE: You probably need to change this, depending on how you store your config.hjson. - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: - -c - | # Download config file from private repo # Log the current commit info in the build log gcloud source repos clone oo-misc1 && (cd oo-misc1 && git log -n1) && cp oo-misc1/gcp_config.hjson config.hjson && cp oo-misc1/octave-online-866c0deeb0d1.json . && rm -rf oo-misc1; # Perform build steps using same distro as the production VM: - name: rockylinux:8 entrypoint: bash timeout: 7200s args: - -c - | # Install dependencies, including Node.js dnf module install -y nodejs:18/common && yum install -y gcc-c++ make python3 && npm config set prefix /workspace && [[ $? == 0 ]] || exit $?; # exit if failure # Build Node.js projects for oo-back # Use npm ci to install deps from lockfiles ( echo "oo npm ci: shared" && cd shared && npm ci --verbose ) && ( echo "oo npm ci: back-filesystem" && cd back-filesystem && npm ci --verbose ) && ( echo "oo npm ci: back-master" && cd back-master && npm ci --verbose ) && ( echo "oo npm ci: shared/stackdriver" && cd shared/stackdriver && npm ci --verbose ) && ( echo "oo npm ci: shared/gcp" && cd shared/gcp && npm ci --verbose ); [[ $? == 0 ]] || exit $?; # exit if failure # Create exit.js to reboot instance echo 'module.exports = require("../shared/gcp/reboot_or_remove_self.js")' > entrypoint/exit.js [[ $? == 0 ]] || exit $?; # exit if failure # Create tar.gz package tar zcf /tmp/$_OUTPUT_FILENAME . && mv /tmp/$_OUTPUT_FILENAME . && ls -ldh `pwd`; timeout: 7200s # Save the tar.gz package to storage substitutions: _OUTPUT_FILENAME: oo_back_snapshot_rocky8_node18.tar.gz artifacts: objects: location: gs://artifacts.octave-online.appspot.com/objects/oo-back/ paths: - $_OUTPUT_FILENAME ================================================ FILE: containers/oo-front/Dockerfile ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ############### # BOILERPLATE # ############### FROM node:18 WORKDIR /root # Install oo-front # Note: paths are relative to repository root RUN mkdir -p /srv/oo/projects COPY config*.hjson /srv/oo/projects/ COPY shared /srv/oo/projects/shared COPY client /srv/oo/projects/client COPY front /srv/oo/projects/front COPY entrypoint /srv/oo/projects/entrypoint RUN \ cd /srv/oo/projects/shared && \ npm ci && \ cd /srv/oo/projects/front && \ npm ci && \ npm install --only=dev && \ npm run build && \ cd /srv/oo/projects/client && \ npm ci && \ npm install --only=dev && \ npm run bower -- --allow-root install && \ npm run grunt ############ # METADATA # ############ # Ports: # 8080 = express EXPOSE 8080/tcp ################## # CONFIGURATIONS # ################## CMD DEBUG=oo:* node /srv/oo/projects/front/dist/app.js ================================================ FILE: containers/oo-front/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: # Pull additional data from GCP, including config.hjson. # NOTE: You probably need to change this, depending on how you store your config.hjson. - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: - -c - | # Download config file from private repo # Copy AppEngine files to the root directory # Log the current commit info in the build log gcloud source repos clone oo-misc1 && (cd oo-misc1 && git log -n1) && cp oo-misc1/gcp_config.hjson config.hjson && cp oo-misc1/octave-online-866c0deeb0d1.json . && find oo-misc1/appengine/oo-front -mindepth 1 -maxdepth 1 -exec cp -R {} . \; && mv static/ front/ && rm -f package-lock.json && rm -rf oo-misc1; # Build the client project and the front server typescript - name: node:18 entrypoint: bash args: - -c - | ( node -v && npm -v && cd shared && npm ci && cd ../client && npm ci && npm run bower -- --allow-root install && npm run grunt ); [[ $? == 0 ]] || exit $?; # exit if failure ( cd front && npm ci && npm run build ) && ( cd shared/gcp && npm ci ); [[ $? == 0 ]] || exit $?; # exit if failure # Create front_setup.js to download translations from GCP echo 'module.exports = require("../shared/gcp/fetch_translations.js")' > entrypoint/front_setup.js [[ $? == 0 ]] || exit $?; # exit if failure # Deploy to AppEngine # Use "gcloud beta app deploy" to work around bug: https://stackoverflow.com/questions/62575138/network-session-affinitytrue-property-of-app-yaml-file-is-not-reflecting-in - name: gcr.io/cloud-builders/gcloud entrypoint: bash timeout: 3600s args: - -c - | gcloud beta app deploy $_DEPLOY_OPTS; timeout: 3600s substitutions: _DEPLOY_OPTS: "--promote" ================================================ FILE: containers/oo-gith/Dockerfile ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ################################### # oo-gith: front-end for Git HTTP # # =============================== # # - nginx # # - utils-auth # ################################### ############### # BOILERPLATE # ############### FROM ubuntu:jammy WORKDIR /root # Disable all prompts when using apt-get ENV DEBIAN_FRONTEND=noninteractive # Core tmpfs directories: VOLUME \ /run \ /tmp # Essential setup RUN \ mkdir -p /srv/oo && \ apt-get update && \ apt-get install -y \ curl \ ca-certificates \ openssl \ apt-transport-https \ gnupg \ supervisor # NodeSource RUN \ curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \ echo 'deb https://deb.nodesource.com/node_16.x jammy main' > /etc/apt/sources.list.d/nodesource.list && \ echo 'deb-src https://deb.nodesource.com/node_16.x jammy main' >> /etc/apt/sources.list.d/nodesource.list #################### # MAIN BUILD RULES # #################### RUN apt-get update && \ apt-get install --no-install-recommends -y \ nodejs \ nginx \ git \ unzip # Install utils-auth # Note: paths are relative to repository root RUN mkdir -p /srv/oo/projects COPY config*.hjson /srv/oo/projects/ COPY utils-auth /srv/oo/projects/utils-auth COPY shared /srv/oo/projects/shared RUN \ cd /srv/oo/projects/shared && npm ci && \ cd /srv/oo/projects/utils-auth && npm ci # Download GitList for static file serving RUN \ curl -L -k https://github.com/octave-online/gitlist/archive/oo.zip -o gitlist.zip && \ unzip gitlist.zip -d /srv/oo ############ # METADATA # ############ # Ports: # 80 = nginx EXPOSE 80/tcp # Additional tmpfs directories: VOLUME \ /run/oosocks \ /var/log/nginx \ /var/lib/nginx ################## # CONFIGURATIONS # ################## # Note: paths are relative to repository root. COPY utils-auth/configs/custom_4xx.html /var/www/html/ COPY containers/oo-gith/supervisord.conf /etc/supervisor/conf.d/oo.conf COPY containers/oo-gith/nginx.conf /etc/nginx/sites-available/oo.conf # TODO: In some environments, an error occurs due to modules-enabled. Why? RUN \ echo "daemon off;" >> /etc/nginx/nginx.conf && \ rm /etc/nginx/modules-enabled/* && \ sed -i "s/ access_log .*/ access_log \/dev\/stdout;/" /etc/nginx/nginx.conf && \ sed -i "s/ error_log .*/ error_log \/dev\/stderr;/" /etc/nginx/nginx.conf && \ (cd /etc/nginx/sites-enabled && rm default) && \ (cd /etc/nginx/sites-enabled && ln -s ../sites-available/oo.conf) && \ export GITH_HOST=$(node -e "console.log(require('/srv/oo/projects/shared').config.gith.hostname)") && \ echo "Gith Host: $GITH_HOST" && \ sed -i "s/oo-utils/$GITH_HOST/g" /etc/nginx/sites-available/oo.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/oo.conf"] ================================================ FILE: containers/oo-gith/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: # Pull additional data from GCP, including config.hjson. # NOTE: You probably need to change this, depending on how you store your config.hjson. - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: - -c - | # Download config file from private repo # Log the current commit info in the build log gcloud source repos clone oo-misc1 && (cd oo-misc1 && git log -n1) && cp oo-misc1/gcp_config.hjson config.hjson && rm -rf oo-misc1; # Build the image - name: 'gcr.io/cloud-builders/docker' args: - build - --tag=gcr.io/$PROJECT_ID/oo-gith:latest - --file=containers/oo-gith/Dockerfile - '.' timeout: 7200s images: - gcr.io/$PROJECT_ID/oo-gith:latest timeout: 7200s ================================================ FILE: containers/oo-gith/nginx.conf ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . # This is the nginx configuration for the Git file history viewer. upstream git_fcgi { # Note: "oo-utils" gets substituted during the Docker build server oo-utils:3013; } upstream php_fpm { # Note: "oo-utils" gets substituted during the Docker build server oo-utils:3023; } upstream utils_auth_service { server unix:/var/run/oosocks/auth.sock; } server { listen 80; server_name oo-git; root /var/www/html; index index.php index.html; error_page 400 401 403 404 500 /custom_4xx.html; location = /custom_4xx.html { internal; auth_request off; } location = /ping { auth_request off; return 200; } # authorization service auth_request /auth; auth_request_set $auth_status $upstream_status; # git-http-backend location ~ ^.*\.git/(HEAD|info/refs|objects/info/.*|git-(upload|receive)-pack)$ { client_max_body_size 0; fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; fastcgi_param GIT_HTTP_EXPORT_ALL ""; fastcgi_param REMOTE_USER $remote_user; fastcgi_param GIT_PROJECT_ROOT /srv/oo/git/repos; fastcgi_param PATH_INFO $uri; fastcgi_pass git_fcgi; include fastcgi_params; } # Web UI location / { root /srv/oo/gitlist-oo; try_files $uri $uri/ @htaccess; } location @htaccess { include fastcgi_params; fastcgi_pass php_fpm; fastcgi_param SCRIPT_FILENAME /srv/oo/gitlist-oo/index.php; fastcgi_param QUERY_STRING $args; fastcgi_param PHP_AUTH_USER $remote_user; fastcgi_param PHP_AUTH_PW $http_authorization; } # Auth service location = /auth { internal; proxy_pass http://utils_auth_service; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; } } ================================================ FILE: containers/oo-gith/supervisord.conf ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . [supervisord] nodaemon=true logfile=/dev/null logfile_maxbytes=0 pidfile=/var/run/supervisord.pid [program:nginx] command=/usr/sbin/nginx # TODO: Add a prefix to the application logs. https://github.com/Supervisor/supervisor/issues/1326 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 [program:utils-auth] command=/srv/oo/projects/utils-auth/app.js user=www-data environment=NODE_ENV=production # TODO: Add a prefix to the application logs. https://github.com/Supervisor/supervisor/issues/1326 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 ================================================ FILE: containers/oo-redirect/cloudbuild.yaml ================================================ # Copyright © 2021, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: # Pull additional data from GCP, including config.hjson. # NOTE: You probably need to change this, depending on how you store your config.hjson. - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: - -c - | # Download config file from private repo # Copy AppEngine files to the root directory # Delete client/app/node_modules since it doesn't point anywhere # Log the current commit info in the build log gcloud source repos clone oo-misc1 && (cd oo-misc1 && git log -n1) && cp oo-misc1/gcp_config.hjson config.hjson && find oo-misc1/appengine/oo-redirect -type f -exec cp {} . \; && rm -f package-lock.json && rm -f client/app/node_modules && rm -rf oo-misc1; # Deploy to AppEngine - name: gcr.io/cloud-builders/gcloud entrypoint: bash timeout: 1800s args: - -c - | gcloud app deploy $_DEPLOY_OPTS; timeout: 1800s substitutions: _DEPLOY_OPTS: "--promote" ================================================ FILE: containers/oos-quick-start/docker-compose.yaml ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ################################################################## # This is a docker-compose.yaml for a "quick start" installation # ################################################################## ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### version: '3' services: utils-gitd: image: octaveonline/utils-gitd:v1.0.0 build: context: "../.." dockerfile: containers/utils-gitd/Dockerfile ports: - "3003:3003" - "9418:9418" volumes: - git:/srv/oo/git read_only: true tmpfs: - /run - /tmp utils-gith: image: octaveonline/utils-gith:v1.0.0 build: context: "../.." dockerfile: containers/utils-gith/Dockerfile ports: - "3013:3013" - "3023:3023" volumes: - git:/srv/oo/git read_only: true tmpfs: - /run - /tmp - /run/php - /srv/oo/gitlist-oo/cache:uid=33 oo-gith: image: octaveonline/oo-gith:v1.0.0 build: context: "../.." dockerfile: containers/oo-gith/Dockerfile ports: - "8008:80" depends_on: - mongod - utils-gith read_only: true tmpfs: - /run - /tmp - /run/oosocks - /var/log/nginx - /var/lib/nginx mongod: image: docker.io/library/mongo:latest ports: - "27017:27017" volumes: - mongodb:/data/db read_only: true # Log all commands. (TODO: consider the performance penalty?) command: --slowms=1 tmpfs: - /run - /tmp redis: image: docker.io/library/redis:latest ports: - "6379:6379" read_only: true tmpfs: - /run - /tmp octave-deps: image: octaveonline/octave-deps:v1.0.0 build: context: "../.." dockerfile: containers/octave-deps/Dockerfile read_only: true octave-stable: image: octaveonline/octave-stable:v1.0.0 build: context: "../.." dockerfile: containers/octave-stable/Dockerfile args: ["FULL_BASE_IMAGE=octaveonline/octave-deps:v1.0.0"] depends_on: ["octave-deps"] octave-pkg: image: octaveonline/octave-pkg:v1.0.0 build: context: "../.." dockerfile: containers/octave-pkg/Dockerfile args: ["FULL_BASE_IMAGE=octaveonline/octave-stable:v1.0.0"] depends_on: ["octave-stable"] octave-oo: image: octaveonline/octave-oo:v1.0.0 build: context: "../.." dockerfile: containers/octave-oo/Dockerfile args: ["FULL_BASE_IMAGE=octaveonline/octave-pkg:v1.0.0"] depends_on: ["octave-pkg"] oo-back: image: octaveonline/oo-back:v1.0.0 build: context: "../.." dockerfile: containers/oo-back/Dockerfile args: ["FULL_BASE_IMAGE=octaveonline/octave-oo:v1.0.0"] depends_on: - utils-gitd - octave-oo - redis volumes: - logs:/srv/logs read_only: true tmpfs: - /run - /tmp oo-front: image: octaveonline/oo-front:v1.0.0 build: context: "../.." dockerfile: containers/oo-front/Dockerfile ports: - "8080:8080" depends_on: - mongod - redis read_only: true tmpfs: - /run - /tmp volumes: git: mongodb: logs: ================================================ FILE: containers/utils-admin/cloudbuild.yaml ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: # Pull additional data from GCP, including config.hjson. # NOTE: You probably need to change this, depending on how you store your config.hjson. - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: - -c - | # Download config file from private repo # Copy AppEngine files to the root directory # Delete client/app/node_modules since it doesn't point anywhere # Log the current commit info in the build log gcloud source repos clone oo-misc1 && (cd oo-misc1 && git log -n1) && cp oo-misc1/gcp_config.hjson config.hjson && find oo-misc1/appengine/utils-admin -type f -exec cp {} . \; && rm -f package-lock.json && rm -f client/app/node_modules && rm -rf oo-misc1; # Deploy to AppEngine - name: gcr.io/cloud-builders/gcloud entrypoint: bash timeout: 1800s args: - -c - | gcloud app deploy $_DEPLOY_OPTS; timeout: 1800s substitutions: _DEPLOY_OPTS: "--promote" ================================================ FILE: containers/utils-gitd/Dockerfile ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ###################################### # utils-gitd: essential Git services # # ================================== # # - git-daemon # # - create-repo-service # ###################################### ############### # BOILERPLATE # ############### # NOTE: The version of git in newer Ubuntus no longer prints "fatal: no matching remote head" # when fetching an empty repository, which OO needs in order to ignore the errorful exit. # For now, use an older Ubuntu LTS release for this container. FROM ubuntu:bionic WORKDIR /root # Disable all prompts when using apt-get ENV DEBIAN_FRONTEND=noninteractive # Core tmpfs directories: VOLUME \ /run \ /tmp # Essential setup RUN \ mkdir -p /srv/oo && \ apt-get update && \ apt-get install -y \ curl \ ca-certificates \ openssl \ apt-transport-https \ gnupg \ supervisor # The repository root is expected to be mounted here: VOLUME /srv/oo/git #################### # MAIN BUILD RULES # #################### RUN apt-get update && \ apt-get install --no-install-recommends -y \ git \ nodejs ############ # METADATA # ############ # Ports: # 3003 = create-repo-service # 9418 = git-daemon EXPOSE 3003/tcp 9418/tcp ################## # CONFIGURATIONS # ################## # Note: paths are relative to repository root. COPY back-filesystem/git/create-repo-service.js /usr/local/bin/create-repo-service COPY containers/utils-gitd/supervisord.conf /etc/supervisor/conf.d/oo.conf RUN \ useradd -m -u 1600 git && \ chmod a+x /usr/local/bin/create-repo-service CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/oo.conf"] ================================================ FILE: containers/utils-gitd/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: - name: 'gcr.io/cloud-builders/docker' args: - build - --tag=gcr.io/$PROJECT_ID/utils-gitd:latest - --file=containers/utils-gitd/Dockerfile - '.' timeout: 7200s timeout: 7200s images: - gcr.io/$PROJECT_ID/utils-gitd:latest ================================================ FILE: containers/utils-gitd/supervisord.conf ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . [supervisord] nodaemon=true logfile=/dev/null logfile_maxbytes=0 pidfile=/var/run/supervisord.pid user=root [program:gitd] command=/usr/bin/git -c daemon.uploadarch=true -c daemon.receivepack=true daemon --verbose --reuseaddr --export-all --base-path=/srv/oo/git /srv/oo/git | awk '{print "[ERROR] " $0}' user=git # TODO: Add a prefix to the application logs. https://github.com/Supervisor/supervisor/issues/1326 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 [program:create-repo-service] command=/usr/local/bin/create-repo-service /srv/oo/git 3003 environment=NODE_ENV=production user=git # TODO: Add a prefix to the application logs. https://github.com/Supervisor/supervisor/issues/1326 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 ================================================ FILE: containers/utils-gith/Dockerfile ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ########################################### # utils-gith: extra Git services for HTTP # # ======================================= # # - php-fpm + GitList # # - fastcgi + git-http-backend # ########################################### ############### # BOILERPLATE # ############### FROM ubuntu:jammy WORKDIR /root # Disable all prompts when using apt-get ENV DEBIAN_FRONTEND=noninteractive # Core tmpfs directories: VOLUME \ /run \ /tmp # Essential setup RUN \ mkdir -p /srv/oo && \ apt-get update && \ apt-get install -y \ curl \ ca-certificates \ openssl \ apt-transport-https \ gnupg \ supervisor # The repository root is expected to be mounted here: VOLUME /srv/oo/git #################### # MAIN BUILD RULES # #################### RUN apt-get update && \ apt-get install --no-install-recommends -y \ git \ nodejs \ php-fpm \ php-cli \ php-json \ php-xml \ unzip \ fcgiwrap # Install GitList # Note: php-fpm defaults to user "www-data" on Ubuntu 18.04 RUN \ curl -L -k https://github.com/octave-online/gitlist/archive/oo.zip -o gitlist.zip && \ unzip gitlist.zip -d /srv/oo && \ cd /srv/oo/gitlist-oo && \ (curl -s http://getcomposer.org/installer | php) && \ php composer.phar install --no-dev ############ # METADATA # ############ # Ports: # 3013 = git-http-backend # 3023 = php-fpm EXPOSE 3013/tcp 3023/tcp # Additional tmpfs directories: VOLUME \ /run/php \ /srv/oo/gitlist-oo/cache ################## # CONFIGURATIONS # ################## # Note: paths are relative to repository root COPY utils-auth/configs/gitlist.ini /srv/oo/gitlist-oo/config.ini COPY containers/utils-gith/supervisord.conf /etc/supervisor/conf.d/oo.conf RUN \ useradd -m -u 1600 git && \ sed -i "s/error_log = .*/error_log = \/dev\/stderr/" /etc/php/7.2/fpm/php-fpm.conf && \ sed -i "s/;daemonize = .*/daemonize = no/" /etc/php/7.2/fpm/php-fpm.conf && \ sed -i "s/listen = .*/listen = 3023/" /etc/php/7.2/fpm/pool.d/www.conf # && \ # sed -i "s/;chroot =/chroot = \/srv\/oo/" /etc/php/7.2/fpm/pool.d/www.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/oo.conf"] ================================================ FILE: containers/utils-gith/cloudbuild.yaml ================================================ # Copyright © 2019, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ######################################################### # NOTE: All local paths are relative to repository root # ######################################################### steps: - name: 'gcr.io/cloud-builders/docker' args: - build - --tag=gcr.io/$PROJECT_ID/utils-gith:latest - --file=containers/utils-gith/Dockerfile - '.' timeout: 7200s timeout: 7200s images: - gcr.io/$PROJECT_ID/utils-gith:latest ================================================ FILE: containers/utils-gith/supervisord.conf ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . [supervisord] nodaemon=true logfile=/dev/null logfile_maxbytes=0 pidfile=/var/run/supervisord.pid [program:git-http-backend] command=/usr/sbin/fcgiwrap -f -s tcp:0.0.0.0:3013 -p /usr/lib/git-core/git-http-backend user=git # TODO: Add a prefix to the application logs. https://github.com/Supervisor/supervisor/issues/1326 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 [program:php-fpm] command=/usr/sbin/php-fpm7.2 # TODO: Add a prefix to the application logs. https://github.com/Supervisor/supervisor/issues/1326 redirect_stderr=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 ================================================ FILE: entrypoint/.eslintrc.yml ================================================ rules: # Allow console.log no-console: - off ================================================ FILE: entrypoint/README.md ================================================ Octave Online Server: Back Server Entrypoint Scripts ==================================================== This directory contains scripts used for actually running the back server. **These scripts are primarilly intended for use with the SELinux implementation.** *back-selinux.js* sets up log files and runs the *back-master* project. This script is useful when running Octave Online Server as a service. If debugging Octave Online Server, you should run *back-master* directory. *oo-install-host.service* is a SystemCTL service that installs the latest version of the GNU Octave Host file (see the *back-octave* directory) to the local instance. It will update the host file every time the machine is booted. This is useful to help release updates to the host file and make sure that all instances are using up-to-date versions. *oo.service* is the main SystemCTL service used in the SELinux implementation. It runs *back-selinux.js* at startup. *oo_utils_auth.service* is a SystemCTL service that runs the *utils-auth* project. For more information, see that project directory. ================================================ FILE: entrypoint/back-selinux.js ================================================ #!/usr/bin/env node /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; // This file is the entrypoint for back-master, intended for use with the "SELinux" backend. The "Docker" backend has its own initialization built in to the Dockerfiles. // This file intentionally has no dependencies on npm modules to make it more portable. const child_process = require("child_process"); const fs = require("fs"); const path = require("path"); const stream = require("stream"); // Print basic information about the process console.log("Daemon PID:", process.pid); console.log("Date:", new Date().toISOString()); // What files will we be loading? const prefix = (__dirname === "/usr/local/bin") ? "/usr/local/share/oo" : path.join(__dirname, ".."); const configFile = path.join(prefix, "shared/config.js"); const exitFile = path.join(prefix, "entrypoint/exit.js"); const spawnDirectory = path.join(prefix, "back-master"); const spawnFile = path.join(prefix, "back-master/app.js"); // Wait until the application code is ready before continuing. Example: network drives being mounted at startup. // eslint-disable-next-line no-constant-condition while (true) { try { fs.statSync(configFile); fs.statSync(spawnDirectory); fs.statSync(spawnFile); break; } catch(err) { console.log("One or more dependencies not available! Trying again in 5 seconds..."); child_process.execSync("sleep 5"); // blocking sleep } } // Load config file dependency const config = require(configFile); // Load exit routine function getExitFunction() { var exit; try { exit = require(exitFile); console.log("Will use exit routine from exit.js"); } catch(err) { if (/Cannot find module/.test(err.message)) { // If exit.js is not provided, set a no-op. exit = function(){}; console.log("Will use no-op exit routine"); } else throw err; } return exit; } // Make log directories function mkdirSyncNoError(path) { try { fs.mkdirSync(path, "0740"); } catch(err) { if (!/EEXIST/.test(err.message)) { throw err; } } } const monitorLogPath = path.join(config.worker.logDir, config.worker.monitorLogs.subdir); const sessionLogPath = path.join(config.worker.logDir, config.worker.sessionLogs.subdir); mkdirSyncNoError(monitorLogPath); mkdirSyncNoError(sessionLogPath); // Create nested session log dirs (the goal of nesting is to reduce the number of files in each directory) function makeSessionLogDirsRecursive(prefix, depth) { if (depth === config.worker.sessionLogs.depth) { return; } for (let i=0; i<16; i++) { let letter = "0123456789abcdef"[i]; let currpath = path.join(prefix, letter); mkdirSyncNoError(currpath); makeSessionLogDirsRecursive(currpath, depth + 1); } } makeSessionLogDirsRecursive(sessionLogPath, 0); // Create log stream let dateStr = new Date().toISOString().replace(/:/g,"-").replace(".","-").replace("T","_").replace("Z",""); let logPath = path.join(monitorLogPath, config.worker.token+"_"+dateStr+".log"); let logFd = fs.openSync(logPath, "a", "0640"); let logStream = fs.createWriteStream(null, { fd: logFd }); console.log("Logging to:", logPath); // Filter the log stream to remove noisy http2 messages: workaround for https://bugs.centos.org/view.php?id=17047 const illegalLineStarts = [ Buffer.from("send"), Buffer.from("recv"), Buffer.from("stream"), Buffer.from("inflatehd"), Buffer.from("deflatehd"), ]; const transformStream = new stream.Transform({ transform(chunk, encoding, callback) { // Note: assumes that chunks always start at a line boundary let i = 0; outer: for (let j = 0; j < chunk.length; j++) { // Check for newline (Unix-style) if (chunk[j] == 0x0A) { let line = chunk.slice(i, j+1); i = j+1; for (let illegal of illegalLineStarts) { if (line.length < illegal.length) { continue; } if (illegal.compare(line, 0, illegal.length) === 0) { // Found a match; skip to the next line. continue outer; } } // No match found; send this line to the log file. this.push(line, encoding); } } callback(); } }); transformStream.pipe(logStream); // Prepare child process environment and copy all environment variables const spawnOptions = { cwd: spawnDirectory, env: { "GNUTERM": "svg", "DEBUG": "*" }, uid: config.worker.uid, gid: config.worker.uid, stdio: ["inherit", "inherit", "pipe"] }; for (var name in process.env) { if (!(name in spawnOptions.env)) { spawnOptions.env[name] = process.env[name]; } } // Signal Handling var sigCount = 0; var spwn; function doExit() { if (sigCount === 0) { console.log("RECEIVED FIRST SIGNAL. Terminating gracefully."); if (spwn) spwn.kill("SIGTERM"); } else if (sigCount < 5) { console.log("RECEIVED SIGNAL 2-5. Ignoring."); } else { console.log("RECEIVED FINAL SIGNAL. Killing child process now."); if (spwn) spwn.kill("SIGKILL"); process.exit(1); } sigCount++; } process.on("SIGINT", doExit); process.on("SIGHUP", doExit); process.on("SIGTERM", doExit); // Spawn loop function runOnce() { console.log(spawnOptions); spwn = child_process.spawn("/usr/bin/env", ["node", spawnFile], spawnOptions); console.log(`Starting child (${spawnFile}) with PID ${spwn.pid}`); spwn.stderr.pipe(transformStream); spwn.once("exit", (code, signal) => { console.log(`Process exited with code ${code}, signal ${signal}`); if (code !== 0) { setTimeout(runOnce, 500); } else { getExitFunction()(); logStream.close(); // also closes the logFd file descriptor } }); } // Run it! runOnce(); ================================================ FILE: entrypoint/exit.js.sample ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Example exit.js that performs a reboot upon process exit. const child_process = require("child_process"); module.exports = function exit() { console.log("Spawning reboot and exiting"); // This needs to be execFileSync (as opposed to execFile) to stop the parent process from exiting and restarting before the child process has had a chance to finish. child_process.execFileSync("sudo", ["reboot"], { stdio: "inherit" }); } ================================================ FILE: entrypoint/oo-front.service ================================================ [Service] ExecStart=/srv/oo/projects/front/app.js StandardOutput=journal StandardError=journal #SyslogIdentifier=oo-front User=oo Group=oo WorkingDirectory=/srv/oo/projects/front Environment=NODE_ENV=production Restart=always [Install] WantedBy=multi-user.target ================================================ FILE: entrypoint/oo-no-restart.service ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . [Service] ExecStart=/usr/local/bin/oo-back-selinux StandardOutput=syslog StandardError=syslog SyslogIdentifier=oo User=1500 Group=1500 Environment=NODE_ENV=production Restart=no KillMode=mixed TimeoutStopSec=60 [Install] WantedBy=multi-user.target ================================================ FILE: entrypoint/oo-reinstall.service ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . [Unit] Description=Install the Octave Online host file [Service] ExecStart=/bin/true ExecStop=/usr/bin/make -C /srv/oo/projects/back-octave install; \ /usr/bin/make -C /srv/oo/projects reinstall-selinux; \ /usr/bin/make -C /srv/oo/projects install-site-m; Type=oneshot RemainAfterExit=yes [Install] WantedBy=multi-user.target ================================================ FILE: entrypoint/oo.service ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . [Service] ExecStart=/usr/local/bin/oo-back-selinux StandardOutput=syslog StandardError=syslog SyslogIdentifier=oo User=1500 Group=1500 Environment=NODE_ENV=production Restart=always KillMode=mixed TimeoutStopSec=60 [Install] WantedBy=multi-user.target ================================================ FILE: entrypoint/policy/octave_online.fc ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . /usr/local/lib/octave(/.*)? gen_context(system_u:object_r:octave_site_t,s0) /tmp/oo-.*(/.*)? gen_context(unconfined_u:object_r:oo_tmp_t,s0) /tmp/.sandbox-*(/.*)? gen_context(unconfined_u:object_r:oo_tmp_t,s0) /srv/oo(/.*)? gen_context(system_u:object_r:srv_oo_t,s0) # See supplement.te/if #/usr/local/bin/oo-back-selinux -- gen_context(system_u:object_r:oo_exec_t,s0) #/usr/lib/systemd/system/oo.* -- gen_context(system_u:object_r:oo_unit_file_t,s0) ================================================ FILE: entrypoint/policy/octave_online.if ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ## ================================================ FILE: entrypoint/policy/octave_online.te ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . policy_module(octave_online, 1.9); ### TO SETUP THIS POLICY FILE: # # 0. Make sure the following packages are installed: # # selinux-policy-devel # policycoreutils-sandbox # selinux-policy-sandbox # # 1. Copy or symlink this file to /etc/selinux/targeted/policy # # 2. Run the following commands as root: # # make -f /usr/share/selinux/devel/Makefile octave_online.pp # semodule -i octave_online.pp # # 3. Apply file permissions: # # restorecon -R -v /usr/local/lib/octave # ### require { type file_t; type home_root_t; type passwd_file_t; type sandbox_t; type sysfs_t; type tmp_t; type unconfined_service_t; type unconfined_t; type unlabeled_t; type urandom_device_t; class chr_file open; class dir { read open search getattr write add_name }; class file { open execute }; class lnk_file read; class process { dyntransition transition sigchld }; class unix_stream_socket { read write ioctl getattr connectto }; } # Create a new file permission context for GNU Octave site files. type octave_site_t; files_type(octave_site_t); # Create file type for temporary files type oo_tmp_t; files_type(oo_tmp_t); # Create file type for OO server files. type srv_oo_t; files_type(srv_oo_t); # Create domains for running the Octave Online process (see supplement.te/if) # type oo_t; # type oo_exec_t; # init_daemon_domain(oo_t, oo_exec_t); # type oo_unit_file_t; # systemd_unit_file(oo_unit_file_t); # Allow system_r to run and spawn sandbox_t # (required when running application as a systemd service) role system_r types sandbox_t; allow unconfined_service_t sandbox_t:process { dyntransition transition }; allow sandbox_t unconfined_service_t:process sigchld; # Allow sandbox applications to communicate over UNIX sockets, which is # required for non-blocking mode. allow sandbox_t unconfined_t:unix_stream_socket { read write ioctl getattr }; allow sandbox_t unconfined_service_t:unix_stream_socket { read write ioctl getattr }; allow sandbox_t self:unix_stream_socket connectto; # I tried making oo_tmp_t, but tmp_t is still being inherited, so # the following rule is required. #allow sandbox_t tmp_t:dir { write add_name create }; # The following line eliminates more errors from the audit file, and # seem to be necessary to enable Octave to load files from the sandbox # home path. allow sandbox_t home_root_t:dir { search getattr }; # The following line enables Octave to access urandom. Without this, # there are a lot of audit errors, and the oo-events.txt results in a # segmentation fault. # NOTE: This could instead be allowed using 'global_ssp' allow sandbox_t urandom_device_t:chr_file open; # Give permissions to read and execute octave_site_t to sandbox allow sandbox_t octave_site_t:file { open execute map }; allow sandbox_t octave_site_t:dir { read open search getattr }; allow sandbox_t octave_site_t:lnk_file read; # Don't-Audit rules for sandbox to clean up log files a bit dontaudit sandbox_t passwd_file_t:file open; dontaudit sandbox_t sysfs_t:dir read; ================================================ FILE: entrypoint/policy/octave_online_supplement.if ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . interface(`oo_systemctl',` gen_require(` type oo_t, oo_unit_file_t; ') systemd_exec_systemctl($1) init_reload_services($1) systemd_read_fifo_file_passwd_run($1) allow $1 oo_unit_file_t:file read_file_perms; allow $1 oo_unit_file_t:service manage_service_perms; ps_process_pattern($1, oo_t) ') interface(`oo_domtrans',` gen_require(` type oo_t, oo_exec_t; ') corecmd_search_bin($1) domtrans_pattern($1, oo_exec_t, oo_t) ') interface(`oo_admin',` gen_require(` type oo_t, oo_unit_file_t; ') allow $1 oo_t:process { ptrace signal_perms }; ps_process_pattern($1, oo_t) oo_systemctl($1) admin_pattern($1, oo_unit_file_t) allow $1 oo_unit_file_t:service all_service_perms; ') ================================================ FILE: entrypoint/policy/octave_online_supplement.te ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . # This is the output of audit2allow when running the process under systemd # and oo_t. I currently am not using it because SELinux has macros that can # automate most of these rules, but I haven't taken the time to read up on # them yet, and this particular part of the application is not as important # for security vulnerabilities. require { type NetworkManager_t; type auditd_t; type bin_t; type crond_t; type device_t; type devlog_t; type dhcpc_t; type docker_t; type fixed_disk_device_t; type fs_t; type fs_t; type fsadm_exec_t; type getty_t; type init_t; type kernel_t; type kernel_t; type loop_control_device_t; type lvm_t; type mongod_t; type mount_exec_t; type mount_var_run_t; type net_conf_t; type oo_t; type passwd_file_t; type passwd_file_t; type policykit_t; type postfix_master_t; type postfix_pickup_t; type postfix_qmgr_t; type proc_t; type random_device_t; type redis_port_t; type redis_t; type root_t; type rsync_exec_t; type sandbox_t; type semanage_store_t; type seunshare_exec_t; type shell_exec_t; type ssh_exec_t; type ssh_home_t; type ssh_port_t; type sshd_t; type sudo_exec_t; type sysfs_t; type sysfs_t; type syslogd_t; type system_dbusd_t; type systemd_logind_t; type tmp_t; type tmp_t; type tuned_t; type udev_t; type unconfined_service_t; type unconfined_t; type unlabeled_t; type unlabeled_t; type user_home_dir_t; type vmblock_t; type vmblock_t; type vmtools_t; class blk_file { read write getattr open ioctl }; class capability { setuid sys_resource setgid chown audit_write dac_override }; class chr_file { write getattr read open ioctl }; class dir { search setattr read create mounton write relabelfrom getattr rmdir remove_name relabelto open add_name }; class fifo_file { write getattr read create unlink open }; class file { append create execute execute_no_trans getattr ioctl link open read rename setattr unlink write }; class filesystem { mount unmount getattr }; class key write; class lnk_file { create unlink read getattr }; class netlink_audit_socket { nlmsg_relay create }; class netlink_route_socket { bind create getattr nlmsg_read }; class process { dyntransition signal sigchld execmem setexec setsched setcurrent setcap }; class sock_file { write create }; class tcp_socket { getopt name_connect create setopt connect getattr shutdown }; class udp_socket { create connect getattr }; class unix_dgram_socket { create connect sendto }; } #============= oo_t ============== allow oo_t NetworkManager_t:dir { getattr search }; allow oo_t NetworkManager_t:file { read open }; allow oo_t auditd_t:dir { getattr search }; allow oo_t auditd_t:file { read open }; allow oo_t bin_t:file { execute execute_no_trans }; allow oo_t crond_t:dir { getattr search }; allow oo_t crond_t:file { read open }; allow oo_t device_t:blk_file { read write ioctl open getattr }; allow oo_t dhcpc_t:dir { getattr search }; allow oo_t dhcpc_t:file { read open }; allow oo_t docker_t:dir { getattr search }; allow oo_t docker_t:file { read open }; allow oo_t fixed_disk_device_t:blk_file { read write getattr open ioctl }; allow oo_t fs_t:filesystem getattr; allow oo_t fs_t:filesystem { mount unmount }; allow oo_t fsadm_exec_t:file { read execute open execute_no_trans }; allow oo_t getty_t:dir { getattr search }; allow oo_t getty_t:file { read open }; allow oo_t init_t:file { read open }; allow oo_t kernel_t:dir { getattr search }; allow oo_t kernel_t:file { read open }; allow oo_t kernel_t:process setsched; allow oo_t kernel_t:unix_dgram_socket sendto; allow oo_t loop_control_device_t:chr_file { read write getattr open ioctl }; allow oo_t lvm_t:dir { getattr search }; allow oo_t lvm_t:file { read open }; allow oo_t mongod_t:dir { getattr search }; allow oo_t mongod_t:file { read open }; allow oo_t mount_exec_t:file { read getattr open execute execute_no_trans }; allow oo_t mount_var_run_t:file { read write getattr open }; allow oo_t net_conf_t:file { read getattr open }; allow oo_t passwd_file_t:file { read getattr open }; allow oo_t policykit_t:dir { getattr search }; allow oo_t policykit_t:file { read open }; allow oo_t postfix_master_t:dir { getattr search }; allow oo_t postfix_master_t:file { read open }; allow oo_t postfix_pickup_t:dir { getattr search }; allow oo_t postfix_pickup_t:file { read open }; allow oo_t postfix_qmgr_t:dir { getattr search }; allow oo_t postfix_qmgr_t:file { read open }; allow oo_t proc_t:file { read getattr open }; allow oo_t random_device_t:chr_file getattr; allow oo_t redis_port_t:tcp_socket name_connect; allow oo_t redis_t:dir { getattr search }; allow oo_t redis_t:file { read open }; allow oo_t root_t:dir mounton; allow oo_t rsync_exec_t:file { read execute open execute_no_trans }; allow oo_t sandbox_t:dir { getattr search }; allow oo_t sandbox_t:file { read open }; allow oo_t sandbox_t:process { dyntransition signal }; allow oo_t self:capability { setuid sys_resource setgid chown audit_write dac_override }; allow oo_t self:key write; allow oo_t self:netlink_audit_socket { nlmsg_relay create }; allow oo_t self:netlink_route_socket { bind create getattr nlmsg_read }; allow oo_t self:process execmem;allow oo_t devlog_t:sock_file write; allow oo_t self:process { execmem setexec setcurrent setcap setsched }; allow oo_t self:tcp_socket { getattr shutdown }; allow oo_t self:tcp_socket { getopt create connect setopt }; allow oo_t self:udp_socket { create connect getattr }; allow oo_t self:unix_dgram_socket { create connect }; allow oo_t semanage_store_t:dir { read open }; allow oo_t semanage_store_t:file { read getattr open }; allow oo_t seunshare_exec_t:file { read getattr open execute execute_no_trans }; allow oo_t shell_exec_t:file { execute execute_no_trans }; allow oo_t ssh_exec_t:file { read getattr open execute execute_no_trans }; allow oo_t ssh_home_t:dir getattr; allow oo_t ssh_home_t:file { read getattr open }; allow oo_t ssh_port_t:tcp_socket name_connect; allow oo_t sshd_t:dir { getattr search }; allow oo_t sshd_t:file { read open }; allow oo_t sudo_exec_t:file { read execute open execute_no_trans }; allow oo_t sysfs_t:dir read; allow oo_t sysfs_t:file { read getattr open }; allow oo_t sysfs_t:lnk_file read; allow oo_t syslogd_t:dir { getattr search }; allow oo_t syslogd_t:file { read open }; allow oo_t system_dbusd_t:dir { getattr search }; allow oo_t system_dbusd_t:file { read open }; allow oo_t systemd_logind_t:dir { getattr search }; allow oo_t systemd_logind_t:file { read open }; allow oo_t tmp_t:dir { setattr read create mounton write relabelfrom rmdir remove_name add_name }; allow oo_t tmp_t:file { rename link setattr }; allow oo_t tmp_t:file { write create unlink open }; allow oo_t tmp_t:lnk_file { create unlink }; allow oo_t tuned_t:dir { getattr search }; allow oo_t tuned_t:file { read open }; allow oo_t udev_t:dir { getattr search }; allow oo_t udev_t:file { read open }; allow oo_t unconfined_t:dir { getattr search }; allow oo_t unconfined_t:file { read open }; allow oo_t unlabeled_t:dir remove_name; allow oo_t unlabeled_t:dir { write rmdir setattr read relabelto create add_name }; allow oo_t unlabeled_t:file { write read create unlink open }; allow oo_t user_home_dir_t:dir { getattr mounton }; allow oo_t vmblock_t:dir { write search getattr add_name }; allow oo_t vmblock_t:file execute_no_trans; allow oo_t vmblock_t:file { execute read create getattr write ioctl open append }; allow oo_t vmblock_t:lnk_file { read getattr }; allow oo_t vmtools_t:dir { getattr search }; allow oo_t vmtools_t:file { read open }; #============= sandbox_t ============== allow sandbox_t oo_t:process sigchld; allow sandbox_t passwd_file_t:file open; allow sandbox_t sysfs_t:dir read; allow sandbox_t unlabeled_t:dir { read write create add_name remove_name }; allow sandbox_t unlabeled_t:fifo_file { write getattr read create unlink open }; allow sandbox_t unlabeled_t:file { write create unlink }; allow sandbox_t unlabeled_t:sock_file { write create }; ================================================ FILE: front/.eslintrc.yml ================================================ parser: '@typescript-eslint/parser' plugins: - '@typescript-eslint' extends: - plugin:@typescript-eslint/eslint-recommended - plugin:@typescript-eslint/recommended rules: # Allow the explicit any type, since there is a lot of interfacing to non-TS libraries '@typescript-eslint/no-explicit-any': off # JSON/Mongo uses snake case '@typescript-eslint/camelcase': off # Explicit void return types are a lot of clutter '@typescript-eslint/explicit-function-return-type': off # The non-null assertion has valid use cases, such as within callback functions '@typescript-eslint/no-non-null-assertion': off # Interfaces that act as interfaces for the purposes of class hierarchy should be prefixed with I; other interfaces, such as those used for declaring object structure, don't need to be prefixed with I '@typescript-eslint/interface-name-prefix': off ================================================ FILE: front/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: front/README.md ================================================ Octave Online Server: Front =========================== The Front server is responsible for running all Socket.IO connections. It also comes with an Express HTTP server that handles authentication-related requests and serves static files. The Front server does not handle HTTPS requests. It is recommended that you put another piece of server software, such as Nginx, in front of the Front server to handle HTTPS. You can also point Nginx to the "dist" directory to serve static files and reduce the number of requests going into Express. ## Building and Running To perform a one-time production bulid: ```bash $ npm ci $ npm build $ NODE_ENV=production DEBUG=oo:* node dist/app.js ``` To perform an auto-updating development build: ```bash $ npm install $ npm watch & $ DEBUG=oo:* node dist/app.js ``` ## Extra Static Files You can create a directory parallel to this file named *static*; the files in this directory will be served by Express. ================================================ FILE: front/locales/README.md ================================================ Octave Online Localization ========================== Octave Online Server supports the localization of UI strings. The source language is English, and the strings are stored in *en.yaml* using the i18next JSON format. Additional translations should be submitted to the oos-translations project: https://github.com/octave-online/oos-translations When new strings are added to *en.yaml*, a corresponding description should be added to *qqq.yaml* to help translators produce more accurate translations. Use the config options *front.locales_path* and *front.locales* to set an alternate load path for the localization files. ================================================ FILE: front/locales/en.yaml ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or # modify it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the License, # or (at your option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . header: sidebar: changeemail#body: Signed in before but need to change your email address? Click "Feedback" and create a ticket. We will change it for you. sharedesc#body: Others can use the following link to connect to your workspace. createnewproject#btn: Create New Project changelayout#btn: Change/Reset Layout changepassword#btn: Change Password changetheme#btn: Change Theme disablesharing#btn@2: Disable Collaboration enablesharing#btn@2: Enable Collaboration enablesharing#tooltip: Enabling collaboration allows others to edit your home workspace in real time. enablesharingp#tooltip: Enabling collaboration allows others to edit this project in real time. github#btn: GitHub Project inlineplots#btn: Render New Plots Inline legal#btn: Privacy Policy and EULA patreon#btn: Upgrade with Patreon signinemail#btn: Sign in with Email signingoogle#btn: Sign in with Google signinpassword#btn: Sign in with Password signout#btn: Sign Out support#btn: Feedback and Support textwrap#btn: Wrap Long Lines in Console twitter#btn: Follow on Twitter buckets: delete#btn@2: Delete Bucket/Project title@2: "Buckets and Projects:" meta: official: title: "{{config.client.app_name}} · Cloud IDE compatible with MATLAB" desc: Create and share scripts for scientific computing with GNU Octave. Free and open-source. Works in your browser, including on Chromebook, iPad, tablet, and smartphone. server: title: "{{config.client.app_name}}" desc: The power of {{config.client.app_name}} run on custom hardware. Used under the AGPL license. students: reenroll#btn: Move unenroll#btn: Unenroll subhead1: "Code:" title: Students site: menu#btn: MENU adfallback#body: "Do your part: support Free Software by contributing US$3/mo." editor: toolbar: create#btn: Create empty file delete#btn: DELETE File download#btn: Download File history#btn: File version history print#btn: Print File refresh#btn: Refresh files rename#btn: Rename File run#btn: Run Script save#btn: Save File share#btn: Share File in new Bucket toolbar#btn: Show Toolbar upload#btn: Upload file wrap#btn: Toggle Word Wrap files#title: Files drop#ui: Drop Files Here to Upload run#ui: RUN tips: a1: Full List dd1: Show the auto-completion menu dd2: Save the file dd3: Run the file dd4: Set focus to the prompt p1: "The files you make on {{config.client.app_name}} will be saved for the next time you visit. They will be deleted after 6 months of inactivity." p2: "Common shortcuts:" subhead1: Keyboard Shortcuts title: Tips & Tricks unsupported: li1: "The file type isn't supported in this editor." li2: "The file is in a binary format, like images." li3: "The file is too large and can't be loaded into the editor." p1: "The currently selected file can't be edited online. You can still use the toolbar above to rename, download, and delete the file. Reasons might include:" title: "{{config.client.app_name}} Interactive Editor" panel: output: github#btn: "{{projectName}} on GitHub" upgraded#ui: Upgraded session plot: download#btn: Download Plot as Image expand#btn: Expand Plot Window next#btn: Next Plot prev#btn: Previous Plot zoom#btn: Click to zoom description#ui: "Plot: Line {{-lineNumberHTML}}" prompt: add#btn: Add {{-numSecondsHTML}} Seconds resume#btn: Resume Execution sigint#btn: Stop Currently Running Command (send SIGINT) upgrade#btn: Upgrade seconds#ui: "Seconds Remaining:" vars: title: Vars promos: instructor: p1: Did you know you can use {{config.client.app_name}} in your class? p2: Open a support ticket to inquire about how we can set you up as an instructor. prompt: btn1: Sign In p1: Type expressions here and press enter. p2: "To use script files:" title: Octave Command Prompt welcome: title: Welcome to {{config.client.app_name}} p1: "{{config.client.app_name}} is a web UI for GNU Octave, the open-source alternative to MATLAB. Thousands of students, educators, and researchers from around the world use {{config.client.app_name}} each day for studying machine learning, control systems, numerical methods, and more." p2: Type commands in the prompt like you would in your local copy of GNU Octave or MATLAB. p3: Plot charts and graphs. p4: "Sign in for more features: script files, buckets, real-time collaboration like Google Docs, extended runtime, and more." start#btn: "Start Using {{config.client.app_name}}" sync: p1: View historical versions of your script files! p2: Click the icon for more info. bucketcreate: p1: Share snapshots of your script files! p2: Click the icon to create a new bucket. sharing: subhead1: "Tip:" p1@2: Enable collaboration to allow others to edit your script files in real time, like Google Docs. login: p1: Want to use scripts? p2: Sign in to create and share script files. bucketsbacklink: p1: Sign in to create your own buckets and use the rest of {{config.client.app_name}}. modals: email: label1: "Enter your email address:" p1: You will be emailed an 11-digit code that you will need to enter on the next screen. If you do not receive your code, please open a support ticket for assistance. password: subhead1: New user? Forgot your password? p1: Use the "email token" sign-in option instead. Once you are signed in, use the "Change Password" option in the menu to set a new password. changepwd: title: Change Password p1: Enter your new password below. To remove your password and disable password-based logins, leave the password field blank and click "Save Password". submit#btn: Save Password welcome_back: title: Welcome Back! p1: It has been a while since you've visited {{config.client.app_name}}. You have a clean workspace for a fresh start. To restore your data from last time, please contact support using the button in the menu. btn1: OK bucketinfo: p1: Click below to make your own copy of this bucket or project. label1: "Created at:" label2: "Forked From:" btn1: "Fork This Bucket" btn1p: "Fork This Project" btn2: "Change Short Link" createbucket: title: Customize Bucket titlep: Customize Project p1@2: A "bucket" is the way to share snapshots of script and data files. A "project" is an editable workspace to organize script and data files. subhead1@2: "Optional: Add More Files to Bucket" subhead1p: "Optional: Copy Files into Project" p2: Select files and click the right arrow button. subhead2: Files to Add subhead3: Files in Bucket subhead3p: Files in Project subhead4: "Optional: Select Main File" p3: The "main" script is automatically run when someone views the bucket. label1: "Main File:" subhead5: "Optional: Custom URL / Short Link" p5: You can set a custom URL for your bucket or project via our short-link service at {{config.redirect.hostname}}. p4: After clicking below, your browser will refresh into your new bucket. You can share the URL of the page to which you are redirected. p4p: After clicking below, your browser will refresh into your new project. submit#btn: Create Bucket submitp#btn: Create Project upgrade: title: Upgrade Account btn1: View Plans btn2: Link Patreon Account p1: Thank you for your {{-pledgeAmountHTML}} pledge! btn3: Change on Patreon p2: Want to transfer your pledge? btn4: Unlink Patreon Account p3: Each week, {{config.client.app_name}} connects tens of thousands of students, educators, and researchers in over 100 countries. We have no paywall. If you believe in our mission to provide educational software free of charge, please join us by supporting {{config.client.app_name}} on Patreon. history: title: File History subtitle1: Version Control p1: Revisions to your files are tracked by a Git version control system. btn1: Launch Version Control subtitle2: Download Snapshot p2: "To request a ZIP file snapshot sent to your email, click the link below. Note: If you edited your files this session, click the \"Refresh Files\" button first to ensure you receive the most up-to-date snapshot." btn2: Generate ZIP Archive patience: p1: Hmm, looks like the server is busy today! We'll get you connected before you know it. Thanks for your patience. p2: Thanks for waiting. It's not supposed to take this long. Please double-check your internet connection. If you are sure that your internet is working, consider contacting support. javascript: console: exited#alert: "Octave Exited. Message:" payload#alert: "NOTICE: Execution paused due to large payload" plotwindow#alert: This button shows and hides the plot window. Enter an expression that generates a plot. readonly#alert@2: "Note: This is a read-only bucket; your edits are temporary. To make your edits permanent, fork the bucket by clicking the bucket name:" reconnecting#alert: "Connection lost. Attempting to reconnect..." refresh#alert: "This will reload your files from the server. Any unsaved changes will be lost." reconnect#btn: Click Here to Reconnect pingtime#label: "Ping time:" seeurl#label: "See:" newfile: label: "Please enter a filename:" helloworld: Hello World print: p1: "Printed for:" p2: Powered by {{config.client.app_name}} rename: alert: "The specified filename already exists." label: "Enter a new filename:" students: course#label: "Course:" name#label: "Name:" enroll: p1: You need to sign in to enroll in a course. p2: When you enroll in a course, the instructors for that course will be able to access the files you save. You can cancel your enrollment at any time by running running the following at the command prompt. p3: Press Cancel if you don't know what any of this means. reenroll: p1: "Enter the course code to which you want to move this student:" p2: The following student is being re-enrolled. Reload the page to see the update. p3: "Error: Could not find the course code:" unenroll: p1: Are you sure that you want to remove the following student from your course? buckets: # Note: These requirements should match the regex in socket_connect.ts error1: "Error: invalid short link. Use at least 5 letters, numbers, -, and _." error2: "Error: That short link is already taken. Please try again." label1: "Enter a new short link:" # Not currently used: togglesharing: p1: "You cannot disable sharing as a student enrolled in an {{config.client.app_name}} course code:" p2: "You need to remove yourself by running this command at at the command prompt:" common: email#ph: email address submit#btn: Submit caution#label: "Caution:" password#ph: password https#btn: Switch to a secured connection (https) https#p: You are using an unsecured connection (http). People might be able to see what you submit. loading#ui: Loading… dismiss#btn: dismiss loginrequired: Please sign in to perform this action bucket: Bucket project: Project constants: shortlink_prefix: https://{{config.redirect.hostname}}/ ================================================ FILE: front/locales/qqq.yaml ================================================ # Copyright © 2020, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or # modify it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the License, # or (at your option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . header: sidebar: changeemail#body: (OBSOLETE) Paragraph of body text sharedesc#body: Paragraph of body text createnewproject#btn: Button text changelayout#btn: Button text changepassword#btn: Button text changetheme#btn: Button text disablesharing#btn@2: Button text enablesharing#btn@2: Button text enablesharing#tooltip: "Tooltip (additional explanation of what a button will do)" enablesharingp#tooltip: "Tooltip (additional explanation of what a button will do)" github#btn: Link text inlineplots#btn: Button text legal#btn: Button text patreon#btn: Button text; "Patreon" should not be translated signinemail#btn: Button text signingoogle#btn: Button text signinpassword#btn: Button text signout#btn: Button text support#btn: Button text textwrap#btn: Button text twitter#btn: Button text buckets: delete#btn@2: Button text title@2: Label, followed by a number meta: official: title: Site Title desc: Paragraph of body tex server: title: Site Title (does not need translation) desc: Paragraph of body tex students: reenroll#btn: Button text unenroll#btn: Button text subhead1: Label, followed by a list title: Section header site: menu#btn: Button text to show the menu (appears prominently on page) adfallback#body: Text shown when advertisement is unavailable editor: toolbar: create#btn: Button text delete#btn: Button text download#btn: Button text history#btn: Button text print#btn: Button text refresh#btn: Button text rename#btn: Button text run#btn: Button text save#btn: Button text share#btn: Button text toolbar#btn: Button text upload#btn: Button text wrap#btn: Button text files#title: Section header drop#ui: Instruction for the user run#ui: Button text to evaluate a script (appears prominently on page) tips: a1: Link text dd1: Description of what a button will do dd2: Description of what a button will do dd3: Description of what a button will do dd4: Description of what a button will do p1: Paragraph of body text p2: Label, followed by a list subhead1: Section header title: Section header unsupported: li1: Error text li2: Error text li3: Error text p1: Error text, ending with a label, followed by a list title: Section header panel: output: github#btn: Link text upgraded#ui: Description when hovering over an image plot: download#btn: Button text expand#btn: Button text next#btn: Button text prev#btn: Button text zoom#btn: Button text description#ui: Paragraph of body text prompt: add#btn: Button text with interpolated number; for example, "Add 15 seconds" resume#btn: Button text sigint#btn: Button text; "SIGINT" should not be translated upgrade#btn: Button text seconds#ui: Label, followed by a number vars: title: Section header promos: instructor: p1: Paragraph of body text p2: Call to action, paragraph of body text prompt: btn1: Call to action, button text p1: Paragraph of body text p2: Label, followed by a call to action button title: Section header welcome: title: Section header (shown prominently on page) p1: Paragraph of body text p2: Paragraph of body text describing what you can do on the site p3: Paragraph of body text describing what you can do on the site p4: Paragraph of body text with a call to action and also describing what you can do on the site start#btn: Call to action, button text sync: p1: Call to action, paragraph of body text p2: Call to action, paragraph of body text bucketcreate: p1: Call to action, paragraph of body text p2: Call to action, paragraph of body text sharing: subhead1: Section header p1@2: Call to action, paragraph of body text login: p1: Rhetorical question asked of the user p2: Call to action, paragraph of body text bucketsbacklink: p1: Call to action, paragraph of body text modals: email: label1: Call to action, form field label p1: Paragraph of body text password: subhead1: Rhetorical question asked of the user p1: Call to action, paragraph of body text changepwd: title: Button text p1: Call to action, paragraph of body text submit#btn: Button text welcome_back: title: Title of dialog box p1: Paragraph of body text btn1: Button text bucketinfo: p1: Call to action, paragraph of body text label1: Label, followed by a date and time label2: Label, followed by a link btn1: Button text btn1p: Button text btn2: Button text createbucket: title: Title of dialog box titlep: Title of dialog box p1@2: Paragraph of body text subhead1@2: Call to action, section header subhead1p: Call to action, section header p2: Call to action, paragraph of body text subhead2: Section header, followed by a list subhead3: Section header, followed by a list subhead3p: Section header, followed by a list subhead4: Call to action, section header p3: Paragraph of body text label1: Section header, followed by a list subhead5: Call to action, section header p5: Paragraph of body text p4: Paragraph of body text p4p: Paragraph of body text submit#btn: Button text submitp#btn: Button text upgrade: title: Title of dialog box btn1: Button text btn2: Button text; "Patreon" should not be translated p1: Paragraph of body text btn3: Button text; "Patreon" should not be translated p2: Rhetorical question asked of the user btn4: Button text; "Patreon" should not be translated p3: Paragraph of body text; "Patreon" should not be translated history: title: Title of dialog box subtitle1: Section title p1: Paragraph of body text btn1: Button text subtitle2: Section title p2: Paragraph of body text btn2: Button text patience: p1: Paragraph of body text p2: Paragraph of body text javascript: console: exited#alert: Label, followed by an error code payload#alert: Paragraph of body text plotwindow#alert: Paragraph of body text with call to action readonly#alert@2: Error message reconnecting#alert: Paragraph of body text refresh#alert: Paragraph of body text reconnect#btn: Button text pingtime#label: Label, followed by a number seeurl#label: Label, followed by a URL link to documentation newfile: label: Label for form field helloworld: Sample string used in programming print: p1: Label, followed by a name or email p2: Body text rename: alert: Body text label: Label for form field students: course#label: Label, followed by a course code (like "Math_101") name#label: Label, followed by a name or email enroll: p1: Paragraph of body text p2: Paragraph of body text p3: Press Cancel if you don't know what any of this means. reenroll: p1: Label for form field p2: Paragraph of body text p3: Label, followed by a course code (like "Math_101") unenroll: p1: Rhetorical question asked of the user buckets: error1: Paragraph of body text error2: Paragraph of body text label1: Label for form field togglesharing: p1: Paragraph of body text, followed by a course code (like "Math_101") p2: Call to action, followed by a computer command common: email#ph: Placeholder text for form field submit#btn: Button text caution#label: Label, followed by another sentence password#ph: Placeholder text for form field https#btn: Button text https#p: Paragraph of body text loading#ui: Filler text when a component is not ready yet dismiss#btn: Button text loginrequired: Error message bucket: Name for a collection of files shared with other users; do not translate in Latin-script languages project: Name for a workspace with files for editing and collaboration constants: shortlink_prefix: DO NOT TRANSLATE ================================================ FILE: front/package.json ================================================ { "name": "@oo/front", "version": "0.0.0", "description": "Front server for Octave Online, 2019 refresh", "main": "dist/app.js", "scripts": { "build": "tsc", "watch": "tsc --watch", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Shane F. Carr", "license": "AGPL-3.0", "engines": { "node": "18.x" }, "dependencies": { "@oo/shared": "file:../shared", "async": "^3.1.0", "base-x": "^3.0.8", "bcrypt": "^5.0.0", "body-parser": "^1.19.0", "compression": "^1.7.4", "connect-mongo": "^4.6.0", "easy-no-password": "^1.3.0", "ejs": "^3.0.1", "express": "^4.17.1", "express-session": "^1.17.0", "flat": "^5.0.2", "hjson": "^3.2.2", "i18next": "^19.5.4", "i18next-fs-backend": "^1.0.7", "i18next-http-middleware": "^3.0.2", "mongoose": "^6.8.2", "npm": "^8.19.3", "ot": "0.0.15", "passport": "^0.6", "passport-google-oauth": "^2.0.0", "passport-local": "^1.0.0", "postmark": "^2.7.0", "pseudo-localization": "^2.1.1", "recaptcha2": "^1.3.3", "serve-static": "^1.14.1", "simple-oauth2": "^3.4.0", "socket.io": "^4", "socketio-file-upload": "^0.7.0", "socketio-wildcard": "^2.0.0", "uuid": "^3.3.3" }, "devDependencies": { "@types/async": "^3.0.3", "@types/bcrypt": "^3.0.0", "@types/body-parser": "^1.17.1", "@types/compression": "^1.0.1", "@types/express": "^4.17.15", "@types/express-session": "^1.17.5", "@types/flat": "^5.0.1", "@types/hjson": "^2.4.3", "@types/passport": "^1.0.2", "@types/passport-google-oauth": "^1.0.41", "@types/passport-local": "^1.0.33", "@types/recaptcha2": "^1.3.0", "@types/serve-static": "^1.13.3", "@types/simple-oauth2": "^2.5.2", "@types/uuid": "^3.4.6", "typescript": "^4.1.3" } } ================================================ FILE: front/src/app.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Fs = require("fs"); import Path = require("path"); import { config, logger } from "./shared_wrap"; import * as Mongo from "./mongo"; import * as Passport from "./passport_setup"; import * as Middleware from "./session_middleware"; import * as ExpressApp from "./express_setup"; import * as SocketIoApp from "./socketio"; const log = logger("app"); async function main() { let buildData: ExpressApp.BuildData = {}; try { buildData = require(Path.join(__dirname, "..", "..", config.front.static_path, "build_data.json")); log.trace("Loaded buildData:", buildData); } catch(err: any) { log.warn("Failed to load buildData; will serve development resources:", err?.message); } try { const setupFn = require("../../entrypoint/front_setup"); await setupFn(buildData); log.trace("Successfully ran front_setup. New buildData:", buildData); } catch (err: any) { if (/Cannot find module/.test(err?.message)) { log.warn("Tip: create entrypoint/front_setup.js to run code at startup:", err?.message); } else { log.error("front_setup error:", err); } } if (!buildData.locales_path) { buildData.locales_path = Path.join(__dirname, "..", "..", config.front.locales_path); } if (!buildData.locales) { buildData.locales = config.front.locales; } buildData.privacy_html = await Fs.promises.readFile(Path.join(__dirname, "..", "..", config.front.static_path, "privacy.txt"), { encoding: "utf-8" }); try { log.trace("Connecting to Mongo..."); await Mongo.connect(); log.info("Connected to Mongo"); } catch(err) { log.warn("Could not connect to Mongo:", err); } Passport.init(); Middleware.init(); ExpressApp.init(buildData); SocketIoApp.init(); } main().catch((err) => { log.error(err); }); ================================================ FILE: front/src/back_server_handler.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import { EventEmitter } from "events"; import { config, newRedisMessenger, newRedisQueue, IRedisQueue, logger, ILogger } from "./shared_wrap"; import { octaveHelper } from "./octave_session_helper"; const outputClient = newRedisMessenger(); outputClient.subscribeToOutput(); const destroyUClient = newRedisMessenger(); destroyUClient.subscribeToDestroyU(); const expireClient = newRedisMessenger(); expireClient.subscribeToExpired(); const redisMessenger = newRedisMessenger(); outputClient.setMaxListeners(100); destroyUClient.setMaxListeners(100); expireClient.setMaxListeners(100); export class BackServerHandler extends EventEmitter { public sessCode: string|null = null; private touchInterval: any; private redisQueue: IRedisQueue|null = null; private _log: ILogger; constructor() { super(); this._log = logger("back-handler:uninitialized"); } public setSessCode(sessCode: string|null) { this.sessCode = sessCode; if (this.redisQueue) { this.redisQueue.reset(); this.redisQueue = null; } if (sessCode) { this.redisQueue = newRedisQueue(sessCode); this.redisQueue.on("message", (name, content) => { this.emit("data", name, content); }); this._log = logger("back-handler:" + sessCode); } else { this._log = logger("back-handler:uninitialized"); } this.touch(); } public dataD(name: string, data: any) { if (this.sessCode === null) { this.emit("data", "alert", "ERROR: Please reconnect! Your action was not performed: " + name); return; } try { redisMessenger.input(this.sessCode, name, data); } catch(err) { this._log.error("ATTACHMENT ERROR", err); } } public subscribe() { // Prevent duplicate listeners this.unsubscribe(); // Create listeners to Redis outputClient.on("message", this.pMessageListener); destroyUClient.on("destroy-u", this.destroyUListener); expireClient.on("expired", this.expireListener); this.touch(); this.touchInterval = setInterval(this.touch, config.redis.expire.interval); } public unsubscribe() { outputClient.removeListener("message", this.pMessageListener); destroyUClient.removeListener("destroy-u", this.destroyUListener); expireClient.removeListener("expired", this.expireListener); clearInterval(this.touchInterval); } private touch = () => { if (!this.depend(["sessCode"])) return; redisMessenger.touchInput(this.sessCode, false); }; private depend(props: string[], log=false) { for (let i = 0; i < props.length; i++){ if (!(this as any)[props[i]]) { if (log) this._log.warn("UNMET DEPENDENCY", props[i], arguments.callee.caller); return false; } } return true; } private pMessageListener = (sessCode: string, name: string, getData: any) => { if (!this.depend(["sessCode"])) return; // Check if this message is for us. if (sessCode !== this.sessCode) return; // Everything from here down will be run only if this instance is associated with the sessCode of the message. this.redisQueue!.enqueueMessage(name, getData); }; private destroyUListener = (sessCode: string, message: any) => { if (!this.depend(["sessCode"])) return; if (sessCode !== this.sessCode) return; this.emit("destroy-u", message); }; private expireListener = (sessCode: string, channel: string) => { if (!this.depend(["sessCode"])) return; if (sessCode !== this.sessCode) return; // If the session becomes expired, trigger a destroy event // both upstream and downstream. this._log.trace("Detected Expired:", channel); octaveHelper.sendDestroyD(this.sessCode, "Octave Session Expired"); this.emit("destroy-u", "Octave Session Expired"); }; } ================================================ FILE: front/src/bucket_model.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Mongoose Bucket: stores metadata about a bucket or project. import Mongoose = require("mongoose"); import { IUser } from "./user_model"; import { logger, ILogger } from "./shared_wrap"; type Err = Error | null; // Initialize the schema const bucketSchema = new Mongoose.Schema({ /// bucket_id is the public ID for the bucket. Generated in socket_connect.ts (formerly in octave-session.js) bucket_id: String, /// user_id is the owner of the bucket. user_id: Mongoose.Schema.Types.ObjectId, /// main is the entrypoint script for the bucket, if the bucket has one. main: String, /// butype is the bucket type: /// - "readonly" = public, read-only (bucket~) /// - "editable" = private, read-write (project~) /// - "collab" = public, read-write, shared workspace (project~) butype: { type: String, enum: ["readonly", "editable", "collab"], default: "readonly", }, /// base_bucket_id is the bucket from which this bucket was cloned, if applicable base_bucket_id: String, /// shortlink is a String accepted by the shortlink service, optimized for ease of manual entry. Can be custom, but the default is generated in client/app/js/bucket.js shortlink: String, last_activity: { type: Date, default: Date.now }, }); bucketSchema.index({ bucket_id: 1 }, { unique: true }); bucketSchema.index({ shortlink: 1 }, { unique: true }); // Workaround to make TypeScript apply signatures to the method definitions interface IBucketMethods { checkAccessPermissions(user: IUser|null): boolean; isOwnedBy(user: IUser|null): boolean; touchLastActivity(next: (err: Err) => void): void; removeRepo(next: (err: Err) => void): void; isValidAction(this: IBucket, action: string): boolean; logf(): ILogger; } export interface IBucket extends Mongoose.Document, IBucketMethods { _id: Mongoose.Types.ObjectId; bucket_id: string; user_id: Mongoose.Types.ObjectId; main?: string; butype: string; base_bucket_id?: string|null; shortlink?: string; last_activity: Date; // Virtuals createdTime: Date; consoleText: string; displayName: string; baseModel: IBucket; ownerModel: IUser; } // Define the methods in a class to help TypeScript class BucketMethods implements IBucketMethods { isValidAction(this: IBucket, action: string): boolean { if (action === "bucket") { return this.butype === "readonly"; } else if (action === "project") { return this.butype === "editable" || this.butype === "collab"; } else { return false; } } checkAccessPermissions(this: IBucket, user: IUser|null): boolean { if (this.butype === "editable" && (!user || !user._id.equals(this.user_id))) { return false; } return true; } isOwnedBy(this: IBucket, user: IUser|null): boolean { if (!user || !user._id.equals(this.user_id)) { return false; } return true; } touchLastActivity(this: IBucket, next: (err: Err) => void): void { this.logf().trace("Touching last activity", this.consoleText); this.last_activity = new Date(); this.save(next); } removeRepo(this: IBucket, next: (err: Err) => void): void { // See comment in socket_connect.ts next(new Error("Unimplemented")); /* (got as Got)("http://" + config.git.hostname + ":" + config.git.createRepoPort, { searchParams: { type: "buckets", name: this.bucket_id, action: "delete", }, retry: 0, }).then((response) => { if (response.statusCode === 200) { next(null); return; } next(new Error(response.body)); }).catch(next); */ } logf(this: IBucket): ILogger { return logger("bucket:" + this.id.valueOf()); } } // Copy the methods into bucketSchema bucketSchema.methods.isValidAction = BucketMethods.prototype.isValidAction; bucketSchema.methods.checkAccessPermissions = BucketMethods.prototype.checkAccessPermissions; bucketSchema.methods.isOwnedBy = BucketMethods.prototype.isOwnedBy; bucketSchema.methods.touchLastActivity = BucketMethods.prototype.touchLastActivity; bucketSchema.methods.removeRepo = BucketMethods.prototype.removeRepo; bucketSchema.methods.logf = BucketMethods.prototype.logf; // Virtuals bucketSchema.virtual("createdTime").get(function (this: IBucket) { return this._id.getTimestamp(); }); bucketSchema.virtual("consoleText").get(function(this: IBucket) { return `[Bucket ${this.bucket_id}; ${this.shortlink}; ${this.butype}${this.base_bucket_id?`; Base ${this.base_bucket_id}`:``}]`; }); bucketSchema.virtual("displayName").get(function(this: IBucket) { // TODO: Localize these strings if (this.butype === "editable" || this.butype === "collab") { return `Project ${this.bucket_id.slice(0, 4)}`; } else { return `Bucket ${this.bucket_id.slice(0, 4)}`; } }); bucketSchema.virtual("baseModel", { ref: "Bucket", localField: "base_bucket_id", foreignField: "bucket_id", justOne: true, }); bucketSchema.virtual("ownerModel", { ref: "User", localField: "user_id", foreignField: "_id", justOne: true, }); bucketSchema.set("toJSON", { virtuals: true }); export const Bucket = Mongoose.model("Bucket", bucketSchema); Bucket.on("index", err => { if (err) logger("bucket-index").error(err); else logger("bucket-index").info("Init Success"); }); ================================================ FILE: front/src/email.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Postmark = require("postmark"); import { config, logger } from "./shared_wrap"; const log = logger("email"); let postmarkClient: Postmark.ServerClient|null = null; if (config.email.provider === "mailgun") { log.warn("Mailgun is no longer supported. Feel free to open a PR to add support. See #43"); } else { postmarkClient = new Postmark.ServerClient(config.postmark.serverToken); } export async function sendLoginToken(email: string, token: string, url: string) { if (postmarkClient) { const response = await postmarkClient.sendEmailWithTemplate({ TemplateAlias: config.postmark.templateAlias, From: config.email.from, To: email, TemplateModel: { product_name: config.email.productName, token_string: token, action_url: url, support_url: config.email.supportUrl } }); log.trace(response); } else { log.error("Unable to send email: please configure an email client for Octave Online"); } } export async function sendZipArchive(email: string, desc: string, url: string) { if (postmarkClient) { const response = await postmarkClient.sendEmailWithTemplate({ TemplateAlias: config.postmark.onDemandSnapshots.template, From: config.email.from, To: email, TemplateModel: { product_name: config.email.productName, archive_desc: desc, action_url: url, support_url: config.email.supportUrl }, MessageStream: config.postmark.onDemandSnapshots.stream, }); log.trace(response); } else { log.error("Unable to send email: please configure an email client for Octave Online"); } } ================================================ FILE: front/src/express_setup.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Http = require("http"); import Path = require("path"); import BodyParser = require("body-parser"); import Compression = require("compression"); import Express = require("express"); import Flatten = require("flat"); import I18next = require("i18next"); import I18nextFsBackend = require("i18next-fs-backend"); import I18nextMiddleware = require("i18next-http-middleware"); import PseudoLocalization = require("pseudo-localization"); import Passport = require("passport"); import ReCAPTCHA = require("recaptcha2"); import ServeStatic = require("serve-static"); import Siofu = require("socketio-file-upload"); import * as SessionMiddleware from "./session_middleware"; import * as Patreon from "./patreon"; import { config, logger } from "./shared_wrap"; const log = logger("express-setup"); const recaptcha = new ReCAPTCHA({ siteKey: config.recaptcha.siteKey, secretKey: config.recaptcha.secretKey, }); const PORT = process.env.PORT || config.front.listen_port; const STATIC_PATH_1 = Path.join(__dirname, "..", "..", config.front.static_path); const STATIC_PATH_2 = Path.join(__dirname, "..", "static"); const STATIC_OPTS: ServeStatic.ServeStaticOptions = { maxAge: "7d", setHeaders: (res, path, stat) => { switch (Path.extname(path)) { case ".html": res.setHeader("Cache-Control", "public, max-age=0"); break; default: break; } } }; export let app: Express.Application; export let server: Http.Server; export interface BuildData { locales_path?: string; locales?: string[]; privacy_html?: string; } function getT(req: Express.Request) { let t = (req as any).t as I18next.TFunction; const requestedLanguage = (req as any).language as string; if (requestedLanguage === "en-XA") { let old_t = t; t = function(key: string, options: any) { return PseudoLocalization.localize(old_t(key, options)); }; } return t; } function getCurrentLanguage(req: Express.Request, buildData: BuildData) { // Figure out the current language from the resource bundles const resolvedLanguages = (req as any).languages as string[]; let currentLanguage; for (let language of resolvedLanguages) { if (buildData.locales!.indexOf(language) !== -1) { currentLanguage = language; break; } } return currentLanguage; } function getJSTranslations(req: Express.Request, t: I18next.TFunction) { // Get the JavaScript translations let oo_translations: {[key: string]: string} = {}; let jsKeys: {[key: string]: unknown} = Flatten((req as any).i18n.getDataByLanguage("en").translation.javascript); for (let key of Object.keys(jsKeys)) { let key_string = key as string; oo_translations[key_string] = t(`javascript.${key_string}`, { config }); } let commonKeys: {[key: string]: unknown} = Flatten((req as any).i18n.getDataByLanguage("en").translation.common); for (let key of Object.keys(commonKeys)) { let key_string = key as string; oo_translations["common." + key_string] = t(`common.${key_string}`, { config }); } let constantsKeys: {[key: string]: unknown} = Flatten((req as any).i18n.getDataByLanguage("en").translation.constants); for (let key of Object.keys(constantsKeys)) { let key_string = key as string; oo_translations["constants." + key_string] = t(`constants.${key_string}`, { config }); } return oo_translations; } export function init(buildData: BuildData){ log.info("Serving static files from:", STATIC_PATH_1); log.info("Loading locales from:", buildData.locales_path); // Work around bug in i18next TypeScript definition? const i18next = (I18next as unknown as I18next.i18n); i18next .use(I18nextMiddleware.LanguageDetector) .use(I18nextFsBackend) .init({ backend: { loadPath: buildData.locales_path!, }, fallbackLng: "en", preload: buildData.locales!, saveMissing: true, missingKeyHandler: function(lng, ns, key, fallbackValue) { log.error("i18next missing key:", lng, ns, key, fallbackValue); }, missingInterpolationHandler: function(text, value) { log.error("i18next missing interpolation:", text, value); } }, (err) => { if (err) { log.error("i18next failed to initialize:", err); } else { log.info("i18next initialized with languages:", Object.keys(i18next.store.data)); } }); app = Express() .use(function(req, res, next) { // Redirect HTTP to HTTPS if (!config.front.require_https) return next(); if (req.headers["x-forwarded-proto"] === "https") return next(); if (req.protocol === "https") return next(); res.redirect(301, `https://${req.hostname}${req.url}`); }) .use(Compression()) .get(/\/[a-z]+~\w+$/, function(req, res, next) { // Internally rewrite the path to index.html req.url = "/index.html"; next("route"); }) .use(ServeStatic(STATIC_PATH_1, STATIC_OPTS)) .use(ServeStatic(STATIC_PATH_2, STATIC_OPTS)) .use(SessionMiddleware.middleware) .use(BodyParser.urlencoded({ extended: true })) .use(BodyParser.json({ verify: function(req, res, buf, encoding) { // Save the buffer for verification later (req as any).rawBody = buf; } })) .use(I18nextMiddleware.handle(i18next)) .use(Passport.initialize()) .use(Passport.session()) .use(Siofu.router) .set("views", Path.join(__dirname, "..", "src", "views")) .set("view engine", "ejs") .get("/ping", function(req, res){ res.sendStatus(204); }) .get(["/", "/index.html"], function(req, res) { res.setHeader("Cache-Control", "public, max-age=0"); const t = getT(req); res.status(200).render("index", { config, buildData, t, oo_translations: getJSTranslations(req, t), currentLanguage: getCurrentLanguage(req, buildData), }); }) .post("/auth/persona", Passport.authenticate("persona"), function(req, res){ res.sendStatus(204); }) .get("/auth/tok", Passport.authenticate("easy", { successRedirect: "/", failureRedirect: "/auth/failure" })) .post("/auth/tok", function(req, res, next) { recaptcha.validateRequest(req, req.ip).then(function(){ // validated and secure log.trace("/auth/tok: ReCAPTCHA OK"); next(); }).catch(function(errorCodes){ // invalid // NOTE: The "p" field is a honeypot in /auth/tok. log.info("/auth/tok: ReCAPTCHA Failure: Query:", JSON.stringify(req.body), "Message:", recaptcha.translateErrors(errorCodes)); res.status(400).render("captcha_error", { config }); }); }, Passport.authenticate("easy"), function(req, res) { res.redirect("/auth/entry?s=" + encodeURIComponent(req.body && req.body.s)); }) .get("/auth/entry", function(req, res) { res.status(200).render("token_page", { config, query: req.query }); }) .post("/auth/pwd", function(req, res, next) { // One star to disable ReCAPTCHA; two stars to enable it /*/ recaptcha.validateRequest(req, req.ip).then(function(){ // validated and secure log.trace("/auth/pwd: ReCAPTCHA OK"); next(); }).catch(function(errorCodes){ // invalid // NOTE: "p" could contain plaintext passwords, so scrub it from the log if ("p" in req.body) req.body.p = ""; log.info("/auth/pwd: ReCAPTCHA Failure: Query:", JSON.stringify(req.body), "Message:", recaptcha.translateErrors(errorCodes)); res.status(400).render("captcha_error", { config }); }); /*/ next(); /**/ }, function(req, res, next) { Passport.authenticate("local", function(err, user, /* info, status */) { if (err) return next(err); if (!user) return res.redirect("/auth/incorrect?s=" + encodeURIComponent(req.body && req.body.s)); // Since we overrode the Passport callback function, we need to manually call res.logIn(). req.logIn(user, {}, function(err) { if (err) return next(err); // Administrative user, first sign-in; generate default fields if (user && !user.parametrized) { log.trace("Administrative User", user.consoleText); user.save().then(() => { log.info("Administrative User Fields Set", user.consoleText); res.redirect("/"); }).catch(next); } else { res.redirect("/"); } }); })(req, res, next); }) .get("/auth/incorrect", function(req, res) { res.status(400).render("incorrect_page", { config, query: req.query }); }) .get("/auth/google", Passport.authenticate("google", { scope: "profile email" })) .get("/auth/google/callback", Passport.authenticate("google", { successRedirect: "/", failureRedirect: "/auth/failure" })) .get("/auth/patreon", Patreon.phase1) .get("/auth/patreon/callback", Patreon.phase2) .get("/auth/patreon/revoke", Patreon.revoke) .get("/auth/patreon/webhook", Patreon.webhook) .post("/auth/patreon/webhook", Patreon.webhook) .get("/auth/patreon/plans", function(req, res) { res.redirect(config.patreon.login_redirect); }) .get("/auth/failure", function(req, res) { res.status(400).render("login_error", { config }); }) .get("/logout", function(req, res, next){ req.logout((err) => { if (err) return next(err); res.redirect("/"); }); }) .get("/js-default/:id.js", function(req, res){ res.setHeader("Content-Type", "text/javascript"); res.end("define('"+req.params.id+"',function(){return function(){}});"); }) .get("*", function(req, res){ res.sendStatus(404); }) .use(function (err: any, req: any, res: any, next: any) { log.error("Express Error", err); res.sendStatus(500); }); server = app.listen(PORT); log.info("Initialized Express Server on port:", PORT); log.debug("App Settings:", (app as any).settings); if (app.get("view cache") && config.front.view_cache_clear_interval) { setInterval(() => { log.trace("Clearing EJS Cache:", Object.keys(require("ejs").cache._data)); require("ejs").clearCache(); }, config.front.view_cache_clear_interval); } } ================================================ FILE: front/src/flavor_record_model.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Mongoose Flavor Record: logs the amount of time using flavor servers. import Mongoose = require("mongoose"); // Initialize the schema const flavorRecordSchema = new Mongoose.Schema({ user_id: Mongoose.Schema.Types.ObjectId, sesscode: String, start: Date, current: Date, flavor: String }); export interface IFlavorRecord extends Mongoose.Document { _id: Mongoose.Types.ObjectId; user_id: Mongoose.Types.ObjectId; sesscode: string; start: Date; current: Date; flavor: string; } export const FlavorRecord = Mongoose.model("FlavorRecord", flavorRecordSchema); ================================================ FILE: front/src/mongo.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Mongoose = require("mongoose"); import { config } from "./shared_wrap"; let client: Mongoose.Mongoose; export async function connect(): Promise { if (!client) { const url = `mongodb://${config.mongo.hostname}:${config.mongo.port}/${config.mongo.db}`; client = await Mongoose.connect(url); } return client; } ================================================ FILE: front/src/octave_session_helper.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Crypto = require("crypto"); import { EventEmitter } from "events"; import { newRedisMessenger, redisUtil, logger, ILogger, config2 } from "./shared_wrap"; type Err = Error|null; export enum SessionState { Needed = "NEEDED", Loading = "LOADING", Live = "LIVE" } const redisMessenger = newRedisMessenger(); class OctaveSessionHelper extends EventEmitter { private _log: ILogger; constructor() { super(); this._log = logger("octave-helper"); } public getNewSessCode(sessCodeGuess: string|null, next: (err: Err, sessCode: string, state: SessionState) => void){ let sessCode = sessCodeGuess; // Check for proper sessCode format (possible attack vector) if (!redisUtil.isValidSessCode(sessCode)) sessCode = null; if (!sessCode) { // Case 1: No sessCode given this.makeSessCode((err,sessCode)=>{ next(err, sessCode, SessionState.Needed); }); } else { this.isValid(sessCode, (state)=> { if (state === SessionState.Needed) { // Case 2: Invalid sessCode given this.makeSessCode((err,sessCode)=>{ next(err, sessCode, SessionState.Needed); }); } else { // Case 3: Valid sessCode given next(null, sessCode!, state); } }); } } public askForOctave(sessCode: string, content: any, next: (err: Err) => void) { const millisecondBoost = content.user ? config2.tier(content.user.tier)["sessionManager.queueBoostTime"] : 0; this._log.trace("Millisecond boost:", millisecondBoost, content.user ? content.user.consoleText : "(no user)"); /* if (content.flavor) { redisMessenger.putSessCodeFlavor(sessCode, millisecondBoost, content.flavor, content); // TODO: Move this call somewhere it could be configurable. rack.createFlavorServer(content.flavor, (err: Err) => { if (err) return this._log.error("RACKSPACE ERROR", err); this._log.trace("Spinning up new server with flavor", content.flavor); }); } else { */ redisMessenger.putSessCode(sessCode, millisecondBoost, content); /* } */ // TODO: Should we do something more interesting for the callback? process.nextTick(() => { next(null); }); } public sendDestroyD(sessCode: string, message: string) { this._log.trace("Sending Destroy-D", message, sessCode); redisMessenger.destroyD(sessCode, message); } private makeSessCode(next: (err: Err, sessCode: string) => void) { Crypto.pseudoRandomBytes(12, (err, buf) => { if (err) { return next(err, "zzz"); } const sessCode = buf.toString("hex"); next(null, sessCode); }); } private isValid(sessCode: string, next: (valid: SessionState) => void) { redisMessenger.isValid(sessCode, (err: Err, valid: string|null) => { if (err) return this._log.error("REDIS ERROR", err); const state = (valid === null) ? SessionState.Needed : ((valid === "false") ? SessionState.Loading : SessionState.Live); next(state); }); } } export const octaveHelper = new OctaveSessionHelper(); ================================================ FILE: front/src/ot_document.ts ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import { EventEmitter } from "events"; import Ot = require("ot"); import Uuid = require("uuid"); import { config, newRedisMessenger, logger, ILogger } from "./shared_wrap"; type Err = Error|null; // Make Redis connections for OT const redisMessenger = newRedisMessenger(); redisMessenger.enableOtScriptsSync(); const otListenClient = newRedisMessenger(); otListenClient.subscribeToOtMsgs(); // A single workspace could account for 50 or more listeners, because each document listens on the same connection. otListenClient.setMaxListeners(200); export class OtDocument extends EventEmitter { private id: string; private chgIds: string[] = []; private crsIds: string[] = []; private touchInterval: any; private _log: ILogger; private _mlog: ILogger; private initialContent: string; public opsReceivedCounter = 0; public setContentCounter = 0; constructor (id: string, safeId: string, initialContent: string) { super(); this.id = id; this._log = logger("ot-doc:" + safeId) as ILogger; this._mlog = logger("ot-doc:" + safeId + ":minor") as ILogger; this.initialContent = initialContent; this.load(); } public logFilename(filename: string) { this._log.trace("Filename:", filename); } public subscribe() { this.unsubscribe(); otListenClient.on("ot-sub", this.otMessageListener); this.touch(); this.touchInterval = setInterval(this.touch, config.ot.document_expire.interval); } public unsubscribe() { otListenClient.removeListener("ot-sub", this.otMessageListener); clearInterval(this.touchInterval); } public dataD(name: string, value: any) { if (this.id !== value.docId) return; switch(name){ case "ot.change": this.onOtChange(value); break; case "ot.cursor": this.onOtCursor(value); break; default: break; } } public changeDocId(newDocId: string) { if (this.id === newDocId) return; const oldDocId = this.id; this.id = newDocId; // Note that multiple clients may all demand the rename simultaneously. // This shouldn't be a problem, as long as at least one of them succeeds. redisMessenger.changeOtDocId(oldDocId, newDocId); } public destroy() { // Same note as above about (many clients performing this simultaneously) redisMessenger.destroyOtDoc(this.id); } private load() { redisMessenger.loadOtDoc(this.id, (err: Err, rev: number, content: string) => { if (err) this._log.error("REDIS ERROR", err); else { this._log.trace("Loaded doc: rev", rev); if (rev === -1) { this.setContent(this.initialContent); this.emit("data", "ot.doc", { docId: this.id, rev: 0, content: this.initialContent }); } else { this.emit("data", "ot.doc", { docId: this.id, rev, content }); } } }); } private touch() { redisMessenger.touchOtDoc(this.id); } private otMessageListener = (docId: string, obj: any) => { if (docId !== this.id) return; let i; switch(obj.type){ case "cursor": if (!obj.data) return; i = this.crsIds.indexOf(obj.data.id); if (i > -1) this.crsIds.splice(i, 1); else { this._mlog.trace("Received another user's cursor:", obj.data.cursor); this.emit("data", "ot.cursor", { docId: this.id, cursor: obj.data.cursor }); } break; case "operation": if (!obj.chgId || !obj.ops) return; i = this.chgIds.indexOf(obj.chgId); if (i > -1) { this._mlog.trace("Received ack for ops:", obj.ops.length); this.chgIds.splice(i, 1); this.emit("data", "ot.ack", { docId: this.id }); } else { this._mlog.trace("Received broadcast of ops:", obj.ops.length); this.emit("data", "ot.broadcast", { docId: this.id, ops: obj.ops }); } break; default: break; } }; private onOtChange = (obj: any) => { if (!obj || typeof obj.op === "undefined" || typeof obj.rev === "undefined") return; const op = Ot.TextOperation.fromJSON(obj.op); this.receiveOperation(obj.rev, op); }; private onOtCursor = (obj: any) => { if (!obj || !obj.cursor) return; const crsId = Uuid.v4(); this.crsIds.push(crsId); redisMessenger.otMsg(this.id, { type: "cursor", data: { id: crsId, cursor: obj.cursor } }); }; private receiveOperation(rev: number, op: Ot.ITextOperation) { const chgId = Uuid.v4(); this._log.trace("Applying operation", rev, chgId); const message = { type: "operation", docId: this.id, chgId: chgId, ops: op.toJSON() }; this.chgIds.push(chgId); redisMessenger.applyOtOperation(this.id, rev, message); this.opsReceivedCounter++; } private setContent(content: string) { const chgId = Uuid.v4(); this._log.trace("Setting content", content.length); const message = { type: "operation", docId: this.id, chgId: chgId }; // note: don't push chgId to this.chgIds so that the operation reply gets sent to our own client redisMessenger.setOtDocContent(this.id, content, message); this.setContentCounter++; } } ================================================ FILE: front/src/passport_setup.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import EasyNoPassword = require("easy-no-password"); import Mongoose = require("mongoose"); import GoogleOAuth = require("passport-google-oauth"); import Local = require("passport-local"); import Passport = require("passport"); import * as Utils from "./utils"; import { config, logger } from "./shared_wrap"; import { User, HydratedUser } from "./user_model"; import { sendLoginToken } from "./email"; type Err = Error | null; const log = logger("passport-setup"); const baseUrl = `${config.front.protocol}://${config.front.hostname}:${config.front.port}/`; const googCallbackUrl = baseUrl + "auth/google/callback"; async function findOrCreateUser(email: string, profile: any) { let user = await User.findOne({ email: email }); // Returning user if (user) { log.info("Returning User", user.consoleText); return user; } // Make a new user log.trace("Creating New User"); user = new User({ email: email, profile: profile }); await user.save(); log.info("New User", user.consoleText); return user; } enum PasswordStatus { UNKNOWN, INCORRECT, VALID } interface UserWithPasswordResponse { user?: HydratedUser; status: PasswordStatus; } async function findWithPasswordPromise(email: string, password: string): Promise { let user = await User.findOne({ email: email }); // Returning user if (user) { // Returning user. Check password let valid = await user.checkPassword(password); if (valid) { return { user, status: PasswordStatus.VALID }; } else { return { user, status: PasswordStatus.INCORRECT }; } } else { return { status: PasswordStatus.UNKNOWN }; } } function findWithPassword(email: string, password: string, done: (err: Err, status?: PasswordStatus, user?: HydratedUser) => void) { findWithPasswordPromise(email, password).then((response) => { done(null, response.status, response.user); }).catch((err) => { done(err); }); } const googleStrategy = new (GoogleOAuth.OAuth2Strategy)({ callbackURL: googCallbackUrl, clientID: config.auth.google.oauth_key, clientSecret: config.auth.google.oauth_secret, userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo" }, function (accessToken, refreshToken, profile, done) { const email = profile.emails?.[0].value; if (!email) { log.warn("No email returned from Google", profile); return done(new Error("No email returned from Google")); } log.info("Google Login", Utils.emailHash(email)); findOrCreateUser(email, profile._json).then((user) => { done(null, user); }, (err) => { done(err); }); }); const easyStrategy = new (EasyNoPassword.Strategy)({ secret: config.auth.easy.secret, maxTokenAge: config.auth.easy.max_token_age }, function (req) { if (req.body && req.body.s) { return { stage: 1, username: req.body.s }; } else if (req.query && req.query.u && req.query.t) { return { stage: 2, username: req.query.u, token: req.query.t }; } else { return null; } }, function (email, token, done) { const url = `${baseUrl}auth/tok?u=${encodeURIComponent(email)}&t=${token}`; sendLoginToken(email, token, url) .then(() => { done(null); }) .catch((err: any) => { log.error("Failed sending email:", email, err); done(err); }); }, function (email: string, done: (err: Err, user?: unknown, info?: any) => void) { log.info("Easy Callback", Utils.emailHash(email)); findOrCreateUser(email, { method: "easy" }).then((user) => { done(null, user); }, (err) => { done(err); }); }); const passwordStrategy = new (Local.Strategy)({ usernameField: "s", passwordField: "p" }, function(username, password, done) { findWithPassword(username, password, function(err, status, user) { if (err) return done(err); log.trace("Password delay", config.auth.password.delay); setTimeout(() => { if (status === PasswordStatus.UNKNOWN) { log.info("Password Callback Unknown User", status, username); return done(null, false); } else if (status === PasswordStatus.INCORRECT) { log.info("Password Callback Incorrect", status, user!.consoleText); return done(null, false); } else if (status == PasswordStatus.VALID) { log.info("Password Callback Success", status, user!.consoleText); return done(null, user); } return done("Invalid password status: " + status); }, config.auth.password.delay); }); } ); export function init(){ Passport.use(googleStrategy); Passport.use(easyStrategy); Passport.use(passwordStrategy); Passport.serializeUser((user: any, cb) => { cb(null, user?.id); }); Passport.deserializeUser((id: Mongoose.Types.ObjectId, cb) => { User.findById(id, cb); }); log.info("Initialized Passport"); } ================================================ FILE: front/src/patreon.ts ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Crypto = require("crypto"); import Async = require("async"); import EasyNoPassword = require("easy-no-password"); import SimpleOAuth2 = require("simple-oauth2"); import { config, logger } from "./shared_wrap"; import { User, HydratedUser } from "./user_model"; interface PatreonInfo { user_id: string; currently_entitled_amount_cents: number; currently_entitled_tier: string|null; } interface PatreonPhase1AsyncAuto { enp: string; raw_user: HydratedUser|null; user: HydratedUser; resolve: any; } interface PatreonPhase2AsyncAuto { enp: boolean; raw_user: HydratedUser|null; user: HydratedUser; tokenObject: any; patreonInfo: PatreonInfo; existingUser: HydratedUser|null; resolve: any; } interface PatreonRevokeAsyncAuto { raw_user: HydratedUser|null; user: HydratedUser; revoke: any; resolve: any; } const log = logger("patreon"); const oauth2 = SimpleOAuth2.create({ client: { id: config.patreon.client_id, secret: config.patreon.client_secret }, auth: { tokenHost: "https://www.patreon.com", tokenPath: "/api/oauth2/token", authorizePath: "/oauth2/authorize" } }); const scope = "identity"; // Use EasyNoPassword to create CSRF tokens for OAuth const enp = EasyNoPassword(config.patreon.state_secret, config.patreon.state_max_token_age); function processMembership(membership: any, user_id: string|null): PatreonInfo { log.trace("Processing membership:", user_id, membership); const currently_entitled_amount_cents = (membership?.attributes?.currently_entitled_amount_cents) as number ?? 0; const tiers = membership?.relationships?.currently_entitled_tiers?.data ?? []; const currently_entitled_tier = (tiers.length > 0) ? (tiers[0].id as string) : null; if (!user_id) { user_id = membership?.relationships?.user?.data.id as string; } if (!user_id) { log.error("Could not find user_id in membership", JSON.stringify(membership)); throw new Error("Could not find Patreon user_id"); } if (!currently_entitled_tier && currently_entitled_amount_cents > 0) { log.warn("No tier, but pledge is > 0:", user_id, JSON.stringify(membership)); } return { currently_entitled_amount_cents, currently_entitled_tier, user_id }; } export function phase1(req: any, res: any, next: any) { Async.auto({ raw_user: (_next) => { const sess = req.session; const userId = sess && sess.passport && sess.passport.user; if (userId) User.findById(userId, _next); else _next(null, null); }, user: ["raw_user", ({raw_user}, _next) => { if (!raw_user) { res.status(400).send("

You must be logged in to perform this action

"); } else if (raw_user.patreon && raw_user.patreon.user_id && !raw_user.patreon.currently_entitled_tier) { log.trace("Patreon already linked", raw_user.consoleText, raw_user.patreon); res.redirect(config.patreon.login_redirect); } else { _next(null, raw_user); } }], enp: ["user", ({}, _next) => { // Don't perform further work until we determine the user exists enp.createToken(req.session.id, _next); }], resolve: ["enp", "user", ({enp, user}, _next) => { log.trace("Patreon phase 1", user.consoleText, req.session.id, enp); const authorizationUri = oauth2.authorizationCode.authorizeURL({ redirect_uri: config.patreon.redirect_url, scope, state: enp }); res.redirect(authorizationUri); }], }, (err: any) => { if (err) { log.error("PATREON PHASE 1 ERROR", err, err.options); } next(err); }); } export function phase2(req: any, res: any, next: any) { if (!req.query || !req.query.state || !req.query.code) { // Login failure? Link denied? res.redirect("/auth/failure"); return; } Async.auto({ raw_user: (_next) => { const sess = req.session; const userId = sess && sess.passport && sess.passport.user; if (userId) User.findById(userId, _next); else _next(null, null); }, user: ["raw_user", ({raw_user}, _next) => { if (!raw_user) { res.status(400).send("

You must be logged in to perform this action

"); } else { _next(null, raw_user); } }], enp: ["user", ({}, _next) => { // Don't perform further work until we determine the user exists enp.isValid(req.query.state, req.session.id, _next); }], tokenObject: ["enp", ({enp}, _next) => { if (!enp) { log.warn("Patreon callback: invalid state", req.session.id, JSON.stringify(req.session)); res.redirect("/auth/failure"); } else { log.trace("Patreon callback: valid state"); oauth2.authorizationCode.getToken({ redirect_uri: config.patreon.redirect_url, code: req.query.code, scope }).then((result) => { log.debug("Success getting token!"); _next(null, result); }).catch((err) => { log.debug("Failure getting token!", err); _next(err); }); } }], patreonInfo: ["tokenObject", ({tokenObject}, _next) => { log.trace(tokenObject); const fullUrl = "https://www.patreon.com/api/oauth2/v2/identity?" + new URLSearchParams({ "include": "memberships,memberships.currently_entitled_tiers", "fields[member]": "currently_entitled_amount_cents" }); log.trace("Requesting URL:", fullUrl); fetch(fullUrl, { headers: { "Authorization": "Bearer " + tokenObject.access_token }, }) .then((response) => { if (!response.ok) { return _next(new Error("Not 2xx response", { cause: response })); } return response.json(); }) .then((body) => { try { const user_id = (body.data.id) as string; log.info("Processing Patreon link for user_id:", user_id); const memberships = body.data.relationships.memberships.data; const membership = (memberships.length > 0) ? body.included[0] : null; const patreonInfo = processMembership(membership, user_id); _next(null, patreonInfo); } catch(cause) { let e = Error("Invalid identity response", { cause }); log.error(e); _next(e); } }).catch(_next); }], existingUser: ["patreonInfo", ({patreonInfo}, _next) => { User.findOne({ "patreon.user_id": patreonInfo.user_id }, _next); }], resolve: ["user", "tokenObject", "patreonInfo", "existingUser", ({user, tokenObject, patreonInfo, existingUser}, _next) => { log.info("Patreon Login Resolved", user.consoleText, JSON.stringify(patreonInfo)); if (existingUser && !existingUser._id.equals(user._id)) { log.warn("Different existing user. Old user:", existingUser.consoleText, "New user:", user.consoleText); res.status(400).render("patreon_link_error", { user_id: patreonInfo.user_id, old_email: existingUser.email, new_email: user.email, config }); return; } // All good. Save the Patreon link to MongoDB. const accessToken = oauth2.accessToken.create(tokenObject); user.patreon = { oauth2: accessToken.token, ...patreonInfo }; user.save().then(() => { if (patreonInfo.currently_entitled_tier) { log.info("Linked account already pledged"); res.redirect("/"); } else { log.info("Redirecting to Patreon to pledge"); res.redirect(config.patreon.login_redirect); } }).catch(_next); }] }, (err: any) => { if (err) { log.error("PATREON PHASE 2 ERROR", err, err.cause, err.options); } next(err); }); } export function revoke(req: any, res: any, next: any) { Async.auto({ raw_user: (_next) => { const sess = req.session; const userId = sess && sess.passport && sess.passport.user; if (userId) User.findById(userId, _next); else _next(null, null); }, user: ["raw_user", ({raw_user}, _next) => { if (!raw_user) { res.status(400).send("

You must be logged in to perform this action

"); } else if (!raw_user.patreon) { res.status(400).send("

Your Patreon account is not linked

"); } else { _next(null, raw_user); } }], revoke: ["user", ({user}, _next) => { log.info("Patreon Revoke", user.consoleText); // TODO: Figure out how to actually revoke the tokens on demand. They will eventually time out. The following code causes an error on the Patreon response: "The content-type is not JSON compatible" /* const accessToken = oauth2.accessToken.create(user.patreon.oauth2); accessToken.revokeAll().then(() => { _next(null); }).catch(_next); */ _next(null); }], resolve: ["user", "revoke", ({user}, _next) => { user.patreon = undefined; user.save().then(() => { res.redirect("/"); }).catch(_next); }], }, (err: any) => { if (err) { log.error("PATREON REVOKE ERROR", err, err.options); } next(err); }); } export function webhook(req: any, res: any, next: any) { const event = req.headers["x-patreon-event"]; const data = req.body ? req.body : req.query; // Verify the signature const hmac = Crypto.createHmac("md5", config.patreon.webhook_secret); hmac.update(req.rawBody); const expectedSignature = hmac.digest("hex"); if (expectedSignature === req.headers["x-patreon-signature"]) { log.trace("Webhook signature matches"); } else { log.warn("Patreon Webhook: Unexpected Signature", req.headers["x-patreon-signature"], event, data); res.sendStatus(412); return; } log.info("Processing Patreon event:", event); let patreonInfo:PatreonInfo; try { const membership = data.data; if (membership && membership.type !== "member") { log.error("ERROR: Expected webhook data to be the membership", data); res.sendStatus(400); return; } patreonInfo = processMembership(membership, null); } catch(e) { log.error("Invalid webhook body:", JSON.stringify(data)); return next(e); } User.findOne({ "patreon.user_id": patreonInfo.user_id }).then((user) => { if (!user) { log.info("Patreon user does not exist in database:", JSON.stringify(patreonInfo)); res.sendStatus(204); return; } log.info("Updating user with Webhook Patreon info", user.consoleText, JSON.stringify(patreonInfo)); Object.assign(user.patreon!, patreonInfo); user.save().then(() => { res.sendStatus(200); }).catch(next); }).catch(next); } ================================================ FILE: front/src/program_model.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Mongoose Program: stores metadata about a program. import Mongoose = require("mongoose"); import { logger, ILogger } from "./shared_wrap"; import { IUser } from "./user_model"; type ProgramModel = Mongoose.Model; // Initialize the schema const programSchema = new Mongoose.Schema({ program_name: String, // Feature overrides tier_override: String, legal_time_override: Number, payload_limit_override: Number, countdown_extra_time_override: Number, countdown_request_time_override: Number, ads_disabled_override: Boolean, }); programSchema.index({ program_name: 1 }); // Workaround to make TypeScript apply signatures to the method definitions interface IProgramMethods { logf(): ILogger; } export interface IProgram { _id: Mongoose.Types.ObjectId; program_name: string; // Feature overrides tier_override?: string; legal_time_override?: number; payload_limit_override?: number; countdown_extra_time_override?: number; countdown_request_time_override?: number; ads_disabled_override?: boolean; // Virtuals students: IUser[] | undefined; } // Virtuals to return results from sub-models programSchema.virtual("students", { ref: "User", localField: "program_name", foreignField: "program", justOne: false, }); programSchema.method("logf", function(): ILogger { return logger("program:" + this.id.valueOf()); } ); programSchema.set("toJSON", { virtuals: true }); export const Program = Mongoose.model("Program", programSchema); Program.on("index", err => { if (err) logger("program-index").error(err); else logger("program-index").info("Init Success"); }); ================================================ FILE: front/src/session_middleware.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import MongoStore = require("connect-mongo"); import Express = require("express"); import ExpressSession = require("express-session"); import Mongoose = require("mongoose"); import { config, logger } from "./shared_wrap"; const log = logger("session-middleware"); export let middleware: Express.RequestHandler; export let store: ExpressSession.Store; export function init() { // Make the store instance if (config.mongo.hostname) { let options = { client: Mongoose.connection.getClient(), }; // Can't solve the following TS error: // "src/session_middleware.ts:39:20 - error TS2589: Type instantiation is excessively deep and possibly infinite" let mongoStore = (MongoStore).create(options); store = mongoStore; } else { log.warn("mongo disabled; using MemoryStore"); store = new ExpressSession.MemoryStore() as ExpressSession.Store; } // Make the middleware instance middleware = ExpressSession({ name: config.front.cookie.name, secret: config.front.cookie.secret, cookie: { maxAge: config.front.cookie.max_age }, store: store, saveUninitialized: false, resave: false, }); log.info("Initialized Session Store"); } ================================================ FILE: front/src/shared_wrap.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import { EventEmitter } from "events"; import * as Shared from "@oo/shared"; export { config, config2, redisUtil } from "@oo/shared"; // Workaround for EventEmitter not being added to shared/index.d.ts export interface IRedisMessenger extends Shared.RedisMessenger, EventEmitter {} export function newRedisMessenger(...args: any[]): IRedisMessenger { return new Shared.RedisMessenger(...args) as IRedisMessenger; } export interface IRedisQueue extends Shared.RedisQueue, EventEmitter {} export function newRedisQueue(...args: any[]): IRedisQueue { return new Shared.RedisQueue(...args) as IRedisQueue; } // This is the interface for @oo/shared/logger, but dts-gen does not generate a full interface for that class. export interface ILogger { trace(...args: any): void; debug(...args: any): void; log(...args: any): void; info(...args: any): void; warn(...args: any): void; error(...args: any): void; } export function logger(id: string): ILogger { return Shared.logger(id) as ILogger; } let gcp: any | undefined; try { gcp = require("../../shared/gcp/index.js"); } catch(e) { logger("shared_wrap").warn("gcp is unavailable:", e); } export { gcp }; ================================================ FILE: front/src/socket_connect.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Async = require("async"); import BaseX = require("base-x"); import Hjson = require("hjson"); import SocketIO = require("socket.io"); import Uuid = require("uuid"); import { BackServerHandler } from "./back_server_handler"; import { Bucket, IBucket } from "./bucket_model"; import { logger, ILogger, gcp } from "./shared_wrap"; import { FlavorRecord } from "./flavor_record_model"; import { IDestroyable, IWorkspace } from "./utils"; import { NormalWorkspace } from "./workspace_normal"; import { SharedWorkspace } from "./workspace_shared"; import { User, HydratedUser } from "./user_model"; import { sendZipArchive } from "./email"; const TOKEN_REGEX = /^\w*$/; const SHORTLINK_REGEX = /^[\p{L}\p{Nd}_-]{5,}$/u; const Base58 = BaseX("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); interface ISocketCustom extends SocketIO.Socket { handler: SocketHandler; removeAllListeners(): this; } interface InitData { action: string|null; info: string|null; oldSessCode: string|null; skipCreate: boolean; flavor: string|null; } interface SocketAsyncAuto { user: HydratedUser|null; raw_init: any; init: InitData; bucket: IBucket|null; init_session: null; } interface DeleteBucketAsyncAuto { bucket: IBucket|null; deleted: any; repo: any; } export class SocketHandler implements IDestroyable { public socket: ISocketCustom; public back: BackServerHandler; public workspace: IWorkspace|null = null; public user: HydratedUser|null = null; public bucket: IBucket|null = null; public flavor: string|null = null; public destroyed = false; private _log: ILogger; public static onConnection(socket: SocketIO.Socket) { const handler = new SocketHandler(socket); handler.socket.handler = handler; } constructor(socket: SocketIO.Socket) { // Set up the socket this.socket = socket as ISocketCustom; this._log = logger("socker-handler:" + socket.id); this._log.info("New Connection", this.socket.handshake.address); this.socket.emit("init"); // Set up Redis connection to back server this.back = new BackServerHandler(); // Add event listeners this.listen(); // Startup tasks Async.auto({ // 1. Load user from database user: (next) => { const sess = (this.socket.request as any).session; const userId = sess?.passport?.user; if (userId) User.findById(userId) .populate("programModel") .exec(next); else next(null, null); }, // 2. Process init data from client and load bucket info raw_init: (next) => { this.socket.once("init", (data) => { next(null, data); }); }, init: ["user", "raw_init", ({user, raw_init}, next) => { raw_init = raw_init || {}; // Send back auth user info this.socket.emit("oo.authuser", { user }); // Process the user's requested action let action = raw_init.action; let info = raw_init.info; let oldSessCode = raw_init.sessCode; let skipCreate = raw_init.skipCreate; const flavor = raw_init.flavor; if (action === "session" && !oldSessCode) { oldSessCode = info; // backwards compat. } // Sanitize the inputs (don't add flavor yet) action = TOKEN_REGEX.test(action) ? action : null; info = TOKEN_REGEX.test(info) ? info : null; oldSessCode = TOKEN_REGEX.test(oldSessCode) ? oldSessCode : null; skipCreate = !!skipCreate; const init = { action, info, oldSessCode, skipCreate, flavor: null }; if (flavor && user) { let flavorOK = user.isFlavorOK(flavor); if (flavorOK) { this._log.info("User connected with flavor:", flavor); init.flavor = flavor; } next(null, init); } else { next(null, init); } }], bucket: ["init", ({init}, next) => { const { action, info } = init; if (action !== "bucket" && action !== "project") { next(null, null); return; } Bucket.findOne({ bucket_id: info as string }) .populate("baseModel") .exec(next); }], // Callback (depends on 1 and 2) init_session: ["user", "init", "bucket", ({user, init, bucket}, next) => { if (this.destroyed) return; // Unpack and save init settings const { action, info, oldSessCode, skipCreate, flavor } = init; this.user = user; this.flavor = flavor; this.bucket = bucket; // Check for a valid bucket if (action === "bucket" || action === "project") { if (!bucket || !bucket.isValidAction(action)) { this._log.info("Invalid bucket/project:", action, (bucket?.consoleText)); this.sendMessage("Unable to find bucket or project: " + (info as string)); this.socket.emit("destroy-u", "Invalid Bucket or Project"); return; } if (!bucket.checkAccessPermissions(user)) { this._log.info("Permission denied:", bucket.consoleText); this.sendMessage("Permission denied: " + bucket.bucket_id); this.socket.emit("destroy-u", "Permission Denied"); return; } this.socket.emit("bucket-info", bucket); } // Fork to load instructor data and buckets this.loadInstructor(); this.loadUserBuckets(); this.touchUser(); this.touchBucket(); if (this.user) { this._log.info("Tier:", this.user.tier); } switch (action) { case "workspace": this._log.warn("Attempted to create no-context workspace:", info); return; case "student": if (!info) return; // Note: this is not necesarilly a student. It can be any user. this._log.info("Attaching to a student's workspace:", info); this.workspace = new SharedWorkspace(info as string, null, null, socket.id); break; case "bucket": case "project": if (!bucket) return; if (bucket.butype === "collab") { // TODO(#41): Initialize the shared workspace with the project owner, not the auth user. Maybe pass null as the user here and then load the project owner inside SharedWorkspace. this._log.info("Attaching to a collaborative project:", bucket.consoleText); this.workspace = new SharedWorkspace(null, user, bucket, socket.id); } else { this._log.info("Attaching to a bucket/project:", bucket.consoleText); this.workspace = new NormalWorkspace(oldSessCode, user, bucket); } break; case "session": default: if (user && user.share_key) { this._log.info("Attaching as host to student's workspace:", user.share_key); this.workspace = new SharedWorkspace(null, user, null, socket.id); } else { this._log.info("Attaching to default workspace with sessCode", oldSessCode); this.workspace = new NormalWorkspace(oldSessCode, user, null); } break; } this.listen(); if (!skipCreate) { this.workspace.beginOctaveRequest(this.flavor); } // Continue down the chain (does not do anything currently) next(null, null); }] }, (err) => { // Error Handler if (err) { this._log.error("ASYNC ERROR", err); } }); } private listen(): void { // Prevent duplicate listeners this.unlisten(); // Make listeners on the socket this.socket.on("*", this.onDataD.bind(this)); this.socket.on("disconnect", this.onDestroyD.bind(this)); // Make listeners on Redis this.back.on("data", this.onDataU.bind(this)); this.back.on("destroy-u", this.onDestroyU.bind(this)); // Make listeners on Workspace if (this.workspace) { this.workspace.on("data", this.onDataW.bind(this)); this.workspace.on("message", this.sendMessage.bind(this)); this.workspace.on("sesscode", this.setSessCode.bind(this)); this.workspace.on("back", this.onDataWtoU.bind(this)); this.workspace.subscribe(); } // Let Redis have listeners too this.back.subscribe(); } private unlisten(): void { this.socket.removeAllListeners(); this.back.removeAllListeners(); this.back.unsubscribe(); if (this.workspace) { this.workspace.removeAllListeners(); this.workspace.unsubscribe(); } } // Convenience function to post a message in the client's console window private sendMessage(message: string): void { // Log to console for backwards compatibility with older clients. // TODO: Remove this and send the alert box only this.socket.emit("data", { type: "stdout", data: message+"\n" }); this.socket.emit("alert", message); } //// MAIN LISTENER FUNCTIONS //// // When the client disconnects (destroyed from downstream) private onDestroyD(): void { this._log.info("Client Disconnect"); if (this.workspace) this.workspace.destroyD("Client Disconnect"); this.unlisten(); } // When the back server exits (destroyed from upstream) private onDestroyU(message: string): void { this._log.info("Upstream Destroyed:", message); this.socket.emit("destroy-u", message); this.back.setSessCode(null); if (this.workspace) this.workspace.destroyU(message); } // When the client sends a message (data from downstream) private onDataD(obj: any): void { const name: string = obj.data[0] || ""; const data: any|null = obj.data[1] || null; // Check for name matches switch(name){ case "init": return; case "enroll": this.onEnroll(data); return; case "update_students": this.onUpdateStudents(data); return; case "oo.unenroll_student": this.onUnenrollStudent(data); return; case "oo.reenroll_student": this.onReenrollStudent(data); return; case "oo.ping": this.onPing(data); return; case "oo.toggle_sharing": this.onToggleSharing(data); return; case "oo.reconnect": this.onOoReconnect(); return; case "oo.set_password": this.onSetPassword(data); return; case "oo.delete_bucket": this.onDeleteBucket(data); return; case "oo.flavor_upgrade": this.onFlavorUpgrade(data); return; case "oo.generate_zip": this.onGenerateZip(data); return; case "oo.create_bucket": this.onCreateBucket(data); return; case "oo.change_bucket_shortlink": this.onChangeBucketShortlink(data); return; default: break; } // Check for prefix matches switch(name.substr(0,3)){ case "ot.": case "ws.": if (this.workspace) this.workspace.dataD(name, data); return; } // Intercept some commands and fork them into the workspace if (name === "data" && this.workspace) this.workspace.dataD(name, data); if (name === "save" && this.workspace) this.workspace.dataD(name, data); // Send everything else upstream to the back server this.back.dataD(name, data); } // When the back server sends a message (data from upstream) // Let (almost) everything continue downstream to the client private onDataU(name: string, data: any): void { if (this.workspace) this.workspace.dataU(name, data); switch(name){ case "bucket-repo-created": this.onBucketRepoCreated(data); return; case "oo.touch-flavor": this.onTouchFlavor(data); break; } this.socket.emit(name, data); } // When the workspace sends a message (data from workspace) // Let everything continue downstream to the client private onDataW(name: string, data: any): void { this.socket.emit(name, data); } //// OTHER UTILITY FUNCTIONS //// private async loadInstructor(): Promise { if (!this.user || !this.user.instructor || !this.user.instructor.length) return; let user = await this.user.loadInstructorModels(); user.instructorModels?.forEach((program) => { this.socket.emit("instructor", { program: program.program_name, users: program.students, }); }); } private async loadUserBuckets(): Promise { if (!this.user) return; let buckets = await Bucket.find({ user_id: this.user._id }); this._log.trace("Loaded", buckets.length, "buckets for user", this.user!.consoleText); this.socket.emit("all-buckets", { buckets }); } private touchUser(): void { if (!this.user) return; this.user.touchLastActivity((err) => { if (err) { this._log.error("TOUCH USER ACTIVITY ERROR", err); return; } }); } private touchBucket(): void { if (!this.bucket) return; this.bucket.touchLastActivity((err) => { if (err) { this._log.error("TOUCH BUCKET ACTIVITY ERROR", err); return; } }); } private onSetPassword(obj: any): void { if (!obj) return; if (!this.user) return; this.user.setPassword(obj.new_pwd, (err) => { if (err) return this._log.error("SET PASSWORD ERROR", err); this.sendMessage("Your password has been changed."); }); } private parseDuplicateKeyError(err: Error): object | void { if (err.message.indexOf("duplicate key error") !== -1) { let dup; try { this._log.trace("Caught duplicate key error:", err.message); dup = Hjson.parse(err.message.substr(err.message.indexOf("dup key:") + 9)); } catch(e) { this._log.error("ERROR: Could not parse duplicate key error:", e, err); return; } return dup; } } private onCreateBucket(obj: any): void { if (!obj) return; if (!this.user) return; if (!obj.shortlink) return; // Validate the shortlink if (!SHORTLINK_REGEX.test(obj.shortlink)) { this.socket.emit("oo.create-bucket-error", { type: "invalid-shortlink" }); return; } // Generate the Bucket ID const bucketIdBuffer = new Buffer(16); Uuid.v4(null, bucketIdBuffer, 0); const bucketId = Base58.encode(bucketIdBuffer); const bucket = new Bucket(); bucket.bucket_id = bucketId; bucket.user_id = this.user._id; bucket.butype = obj.butype; bucket.base_bucket_id = obj.base_bucket_id; bucket.shortlink = obj.shortlink; if (obj.butype === "readonly") { bucket.main = obj.main; } bucket.save((err) => { if (err) { let dup = this.parseDuplicateKeyError(err); if (dup) { this._log.info("Failed to create bucket with duplicate key:", dup, bucket.consoleText, this.user!.consoleText); this.socket.emit("oo.create-bucket-error", { type: "duplicate-key", data: dup, }); return; } else { this._log.error("ERROR from Mongo:", err); return; } } // Success creating the Mongo entry. Now create the repo. this._log.info("Created bucket:", bucket.consoleText, this.user!.consoleText); const backData = bucket.toJSON(); backData.filenames = obj.filenames; this.back.dataD("oo.create_bucket", backData); }); } private onBucketRepoCreated(bucket: any): void { this.socket.emit("bucket-created", { bucket }); } private onChangeBucketShortlink(obj: any): void { const bucket = this.bucket; if (!bucket) return; if (!this.user) return; if (!this.user._id.equals(bucket.user_id)) { this._log.warn("Attempt to change shortlink for another user's bucket"); return; } if (bucket.shortlink !== obj.old_shortlink) { this._log.warn("Attempt to change stale shortlink:", bucket.consoleText, obj); return; } // Validate the shortlink if (!SHORTLINK_REGEX.test(obj.new_shortlink)) { this.socket.emit("oo.change-bucket-shortlink-response", { success: false, type: "invalid-shortlink" }); return; } // Attempt to save the new shortlink this._log.info("Changing shortlink:", bucket.consoleText, obj.new_shortlink); bucket.shortlink = obj.new_shortlink; bucket.save((err) => { if (err) { let dup = this.parseDuplicateKeyError(err); if (dup) { this._log.info("Failed to update bucket with duplicate key:", dup, bucket.consoleText, this.user!.consoleText); this.socket.emit("oo.change-bucket-shortlink-response", { success: false, type: "duplicate-key", data: dup, }); // Reset the shortlink for future calls bucket.shortlink = obj.old_shortlink; return; } else { this._log.error("ERROR from Mongo:", err); return; } } this.socket.emit("oo.change-bucket-shortlink-response", { success: true, bucket }); }); } private onDeleteBucket(obj: any): void { if (!obj) return; if (!obj.bucket_id) return; if (!this.user) return; this._log.info("Deleting bucket:", obj.bucket_id); Async.auto({ bucket: (next) => { Bucket.findOne({ bucket_id: obj.bucket_id }, next); }, deleted: ["bucket", ({bucket}, next) => { if (!bucket) { next(new Error("Unable to find bucket")); return; } if (!bucket.isOwnedBy(this.user)) { next(new Error("Bad owner: " + bucket.consoleText + " " + this.user?.consoleText)); return; } bucket.remove(next); }], repo: ["bucket", "deleted", ({bucket}, next) => { // Note: It would be nice to request repo removal here, but there may be back server instances running that still require it. Instead, there should be a job that periodically scans for and removes orphaned bucket repos. if (bucket) { // bucket.removeRepo(next); next(null); } else { next(null); } }], }, (err) => { if (err) { this._log.warn("DELETE BUCKET", err); this.sendMessage("Encountered error while deleting bucket"); } else { this.socket.emit("bucket-deleted", { bucket_id: obj.bucket_id }); } }); } private onTouchFlavor(obj: any): void { if (!obj) return; if (!this.user) { this._log.error("ERROR: No user on a flavor session"); return; } // Step 1: insert a new FlavorRecord const flavorRecord = new FlavorRecord(); flavorRecord.user_id = this.user._id; flavorRecord.sesscode = this.back.sessCode || "null"; flavorRecord.start = new Date(obj.start); flavorRecord.current = new Date(obj.current); flavorRecord.flavor = obj.flavor; flavorRecord.save((err) => { if (err) return this._log.error("ERROR creating FlavorRecord:", err); // Step 2: delete older FlavorRecords from the same sessCode FlavorRecord.deleteMany({ sesscode: flavorRecord.sesscode, current: { $lt: flavorRecord.current } }, (err /* , writeOpResult */) => { if (err) return this._log.error("ERROR deleting old FlavorRecords:", err); // this._log.trace("Added new FlavorRecord and deleted " + (writeOpResult && writeOpResult.result && writeOpResult.result.n) + " old ones"); }); }); } private onOoReconnect(): void { if (this.workspace) { this.workspace.beginOctaveRequest(this.flavor); } else { this.socket.emit("destroy-u", "Invalid Session"); } } private setSessCode(sessCode: string): void { // We have our sessCode. this._log.info("SessCode", sessCode); this.back.setSessCode(sessCode); this.socket.emit("sesscode", { sessCode: sessCode }); if (this.workspace) this.workspace.sessCode = sessCode; } private onDataWtoU(name: string, value: any): void { this.back.dataD(name, value); } //// ENROLLING AND STUDENTS LISTENER FUNCTIONS //// private onEnroll(obj: any): void { if (!this.user || !obj) return; const program = obj.program; if (!program) return; this._log.info("Enrolling", this.user.consoleText, "in program", program); this.user.program = program; this.user.save((err)=> { if (err) return this._log.error("MONGO ERROR", err); this.sendMessage("Successfully enrolled"); }); } private onUpdateStudents(obj: any): void { if (!obj) return; return this.sendMessage("The update_students command has been replaced.\nOpen a support ticket for more information."); } private async onUnenrollStudent(obj: any): Promise { if (!obj) return; if (!obj.userId) return; if (!this.user) return; let student = await User.findById(obj.userId); if (!student) return this._log.warn("Warning: student not found", obj.userId); if (this.user!.instructor.indexOf(student.program) === -1) return this._log.warn("Warning: illegal call to unenroll student"); this._log.info("Un-enrolling", this.user!.consoleText, "from program", student.program); student.program = "default"; await student.save(); this.sendMessage("Student successfully unenrolled: " + student.displayName); } private async onReenrollStudent(obj: any): Promise { if (!obj) return; if (!obj.userId) return; if (!obj.program) return; if (!this.user) return; if (this.user.instructor.indexOf(obj.program) === -1) { this.sendMessage("Student not re-enrolled: Cannot use the course code " + obj.program); return this._log.warn("Warning: illegal call to re-enroll student"); } let student = await User.findById(obj.userId); if (!student) return this._log.warn("Warning: student not found", obj.userId); if (this.user!.instructor.indexOf(student.program) === -1) return this._log.warn("Warning: illegal call to reenroll student"); this._log.info("Re-enrolling", this.user!.consoleText, "from program", student.program, "to program", obj.program); student.program = obj.program; await student.save(); this.sendMessage("Student successfully re-enrolled: " + student.displayName); } private onPing(obj: any): void { if (!obj) return; this.socket.emit("oo.pong", { startTime: parseInt(obj.startTime) }); } private onToggleSharing(obj: any): void { const enabled: boolean = obj?.enabled; const warning = new Error("warning"); Async.series([ (next) => { if (this.bucket) { if (!this.bucket.isOwnedBy(this.user)) { this.sendMessage("Permission denied"); this._log.warn("Could not toggle sharing: permission denied:", this.bucket.consoleText, this.user?.consoleText); next(warning); } else if (this.bucket.butype === "editable" && enabled) { this.bucket.butype = "collab"; this.bucket.save(next); } else if (this.bucket.butype === "collab" && !enabled) { if (this.workspace) this.workspace.destroyD("Sharing Disabled"); this.bucket.butype = "editable"; this.bucket.save(next); } else { this.sendMessage("Could not toggle sharing"); this._log.warn("Could not toggle sharing:", this.bucket.consoleText, obj); next(warning); } } else if (this.user) { if (this.user.program && this.user.program !== "default") { this.sendMessage("You must unenroll before changing sharing settings.\nTo unenroll, run the command \"enroll('default')\"."); next(warning); } else if (!this.user.share_key && enabled) { this.user.createShareKey(next); } else if (this.user.share_key && !enabled) { if (this.workspace) this.workspace.destroyD("Sharing Disabled"); this.user.removeShareKey(next); } else { this.sendMessage("Could not toggle sharing"); this._log.warn("Could not toggle sharing:", this.user.consoleText, obj); next(warning); } } } ], (err) => { if (err === warning) { // no-op } else if (err) { this._log.error("TOGGLE SHARING", err); this.sendMessage("Could not toggle sharing"); } else { this.socket.emit("reload", {}); } }); } private onFlavorUpgrade(obj: any): void { if (!this.user || !obj) return; const flavor = obj.flavor; const flavorOK = this.user.isFlavorOK(flavor); if (!flavorOK) { this._log.warn("Failed to upgrade user to flavor:", flavor); return; } if (!this.workspace) { this._log.warn("No workspace on flavor upgrade attempt"); return; } this._log.info("User upgraded to flavor:", flavor); this.flavor = flavor; this.workspace.destroyD("Flavor Upgrade"); this.workspace.beginOctaveRequest(this.flavor); } private onGenerateZip(obj: any): void { if (!this.user && !this.bucket) { this._log.error("Nothing to archive:", obj); return; } if (!gcp) { this._log.warn("Cannot generate zip: gcp unavailable"); return; } const log = logger("create-repo-snapshot:" + this.socket.id); let [tld, name, desc] = (this.bucket) ? ["buckets", this.bucket.bucket_id, this.bucket.displayName] : ["repos", this.user!.parametrized, this.user!.displayName]; this.sendMessage("Your zip archive is being generated…"); gcp.uploadRepoSnapshot(log.log, tld, name).then((url: any) => { this.socket.emit("data", { type: "url", url: url, linkText: "Zip Archive Ready", }); if (this.user) { sendZipArchive(this.user.email, desc, url).catch((err) => { log.error("Error sending email:", err); }); } }).catch((err: any) => { this._log.error("onGenerateZip:", err); }); } } ================================================ FILE: front/src/socketio.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import SocketIO = require("socket.io"); import SocketIOWildcard = require("socketio-wildcard"); import * as ExpressApp from "./express_setup"; import * as Middleware from "./session_middleware"; import { SocketHandler } from "./socket_connect"; import { config, logger } from "./shared_wrap"; const log = logger("oo-socketio"); // TODO: Consider using proper TypeScript types: // https://socket.io/docs/v4/typescript/ export function init(){ /* const io = */ new SocketIO.Server(ExpressApp.server, { path: config.front.socket_io_path, allowEIO3: true }) .use(SocketIOWildcard()) .use((socket, next)=>{ // Parse the session using middleware // Bypass TypeScript since this isn't legal (Middleware.middleware)(socket.request, {}, next); }) .on("connection", SocketHandler.onConnection); /* if (config.rackspace.username !== "xxxxxxxxx") { // eslint-disable-next-line @typescript-eslint/no-use-before-define watchFlavorServers(io); } */ log.info("Initialized Socket.IO Server", config.front.socket_io_path); } /* const ALL_FLAVORS = Object.keys(config.flavors); export function watchFlavorServers(io: SocketIO.Server) { Async.forever( (next: () => void) => { Async.map(ALL_FLAVORS, (flavor, _next) => { // TODO: Move this call somewhere it could be configurable. rack.getFlavorServers(flavor, _next); }, (err, results) => { if (err) { log.error("RACKSPACE ERROR", err); } else { results = results as any[]; const rawServers = Array.prototype.concat.apply([], results.map((data) => { return (data as any).servers; })); const servers = rawServers.map((server) => { const { name, created, status } = server; return { name, created, status }; }); io.emit("oo.flavor-list", { servers }); log.debug("Flavor Servers:"); servers.forEach(({ name, created, status }) => { log.debug(name + " " + status + " " + created); }); } setTimeout(next, config.front.flavor_log_interval); }); }, (err: Error|null|undefined) => { log.error("FOREVER ERROR", err); } ); } */ ================================================ FILE: front/src/user_model.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Mongoose User: stores OpenID information for a user. import Bcrypt = require("bcrypt"); import Crypto = require("crypto"); import Mongoose = require("mongoose"); import * as Utils from "./utils"; import { config, logger, ILogger } from "./shared_wrap"; import { Program, IProgram } from "./program_model"; type Err = Error | null; type UserModel = Mongoose.Model; type UserSchema = Mongoose.Schema; export type HydratedUser = Mongoose.HydratedDocument; // Initialize the schema const userSchema: UserSchema = new Mongoose.Schema({ email: String, parametrized: String, profile: Mongoose.Schema.Types.Mixed, openid: { identifier: String, profile: Mongoose.Schema.Types.Mixed }, repo_key: String, share_key: String, password_hash: String, flavors_enabled: Boolean, patreon: { user_id: String, currently_entitled_amount_cents: Number, currently_entitled_tier: String, oauth2: Mongoose.Schema.Types.Mixed }, last_activity: { type: Date, default: Date.now }, program: String, instructor: [String], // Feature overrides tier_override: String, legal_time_override: Number, payload_limit_override: Number, countdown_extra_time_override: Number, countdown_request_time_override: Number, ads_disabled_override: Boolean, }); userSchema.index({ share_key: 1 }); interface IUserMethods { createShareKey(next?: (err: Err) => void): void; removeShareKey(next?: (err: Err) => void): void; setPassword(password: string, next?: (err: Err) => void): void; checkPassword(password: string): Promise; touchLastActivity(next: (err: Err) => void): void; loadInstructorModels(): Promise; isFlavorOK(flavor: string): boolean; logf(): ILogger; } interface IUserVirtuals { displayName: string; consoleText: string; tier: string; programModel: IProgram | null | undefined; instructorModels: IProgram[] | undefined; } export interface IUser { _id: Mongoose.Types.ObjectId; email: string; parametrized: string; profile: any; openid: { identifier: string; profile: any; }; repo_key: string; share_key?: string; password_hash: string; flavors_enabled: boolean; patreon?: { user_id: string; currently_entitled_amount_cents: number; currently_entitled_tier: string|null; oauth2: any; tier_name?: string; // virtual }; last_activity: Date; program: string; instructor: string[]; // Feature overrides tier_override?: string; legal_time_override?: number; payload_limit_override?: number; countdown_extra_time_override?: number; countdown_request_time_override?: number; ads_disabled_override?: boolean; // Cached sub-models _instructorModels?: IProgram[]; } // Parametrization function used by Octave Online, // c. January 2015 - ? function v2Parametrize(id: Mongoose.Types.ObjectId, email: string) { // Represent a name such as "a.b@c.org" like "a_b_c_org". // Note that this method may have funny results with non-english // email addresses, if such a thing exists. // // Adds a human-readable characteristic to the filename. // const param_email = email .trim() .replace(/[-\s@.]+/g, "_") // replace certain chars with underscores .replace(/[^\w]/g, "") // remove all other non-ASCII characters .replace(/([A-Z]+)/g, "_$1") // add underscores before caps .replace(/_+/g, "_") // remove duplicate underscores .replace(/^_/, "") // remove leading underscore if it exists .toLowerCase(); // convert to lower case // Add an arbitrary, but deterministic, eight characters to the // begining of the filename. // // Helps with indexing of filenames, and mostly prevents filename // collisions arising from similar email addresses. // let param_md5 = Crypto .createHash("md5") .update(id.toString()) .digest("base64") .replace(/[^a-zA-Z]/g, "") .toLowerCase(); // In the rare case when this returns a string less than eight, // characters, add arbitrary characters to the end. // param_md5 = (param_md5+"00000000").substr(0, 8); // Concatenate it together. // return param_md5 + "_" + param_email; } // Returns the user's display name userSchema.virtual("displayName").get(function() { if (this.profile && this.profile.displayName) return this.profile.displayName; if (this.openid && this.openid.profile && this.openid.profile.displayName) return this.openid.profile.displayName; if (this.email) return this.email; return "Signed In"; }); // Returns a string containing information about this user // May 2018: Do not log email in consoleText userSchema.virtual("consoleText").get(function() { const safeEmail = Utils.emailHash(this.email); const safeParameterized = this.parametrized && this.parametrized.substr(0, 8); return "[User " + this.id + "; " + safeEmail + "; param:" + safeParameterized + "_…]"; }); // Return the tier for this user, including resource-specific overrides. These items usually fall back to the default unless a value is explicitly set in the database. The camel-case name of these fields is for backwards compatibility. const validTiers = Object.keys(config.tiers); // TODO: Remove the unnecessary `this: HydratedUser`: // https://github.com/Automattic/mongoose/pull/12874 userSchema.virtual("tier").get(function(this: HydratedUser) { // First try: tier_override let candidate: string|undefined = this.tier_override; if (candidate && validTiers.indexOf(candidate) !== -1) { return candidate; } // Second try: Patreon const patreonTier = this.patreon?.currently_entitled_tier; if (patreonTier) { // cast: https://stackoverflow.com/a/35209016/1407170 candidate = (config.patreon.tiers as any)[patreonTier].oo_tier; if (candidate && validTiers.indexOf(candidate) !== -1) { return candidate; } } // Third try: program candidate = (this as HydratedUser).programModel?.tier_override; if (candidate && validTiers.indexOf(candidate) !== -1) { return candidate; } // Default value: return validTiers[0]; }); // Returns the Patreon tier name for the user userSchema.virtual("patreon.tier_name").get(function() { const patreonTier = this.patreon?.currently_entitled_tier; if (patreonTier) { // cast: https://stackoverflow.com/a/35209016/1407170 return (config.patreon.tiers as any)[patreonTier].name; } }); // Virtuals to return results from sub-models userSchema.virtual("programModel", { ref: "Program", localField: "program", foreignField: "program_name", justOne: true, }); userSchema.virtual("instructorModels").get(function() { return this._instructorModels; }); // Add all of the resource-specific overrides to work in the same way. [ { field: "legalTime", overrideKey: "legal_time_override", tierKey: "session.legalTime.user", defaultValue: config.session.legalTime.user }, { field: "payloadLimit", overrideKey: "payload_limit_override", tierKey: "session.payloadLimit.user", defaultValue: config.session.payloadLimit.user }, { field: "countdownExtraTime", overrideKey: "countdown_extra_time_override", tierKey: "session.countdownExtraTime", defaultValue: config.session.countdownExtraTime }, { field: "countdownRequestTime", overrideKey: "countdown_request_time_override", tierKey: "session.countdownRequestTime", defaultValue: config.session.countdownRequestTime }, { field: "adsDisabled", overrideKey: "ads_disabled_override", tierKey: "ads.disabled", defaultValue: config.ads.disabled } ].forEach(({field, overrideKey, tierKey, defaultValue})=>{ // TODO: Remove the unnecessary `this: HydratedUser`: // https://github.com/Automattic/mongoose/pull/12874 userSchema.virtual(field).get(function(this: HydratedUser) { // cast: https://stackoverflow.com/a/35209016/1407170 let candidate: any = (this as any)[overrideKey]; if (candidate) { return candidate; } // cast: https://stackoverflow.com/a/35209016/1407170 candidate = (this.programModel as any|null)?.[overrideKey]; if (candidate) { return candidate; } // cast: https://stackoverflow.com/a/35209016/1407170 candidate = (config.tiers as any)[this.tier]?.[tierKey]; if (candidate) { return candidate; } return defaultValue; }); }); function randomAlphaString(length: number): string { let str = ""; while (str.length < length) { str += Crypto .createHash("md5") .update(Math.random().toString()) .digest("base64") .replace(/[^a-zA-Z]/g, ""); } return str.substr(0, length); } // Auto-fill static fields once, upon creation (or update for old users) userSchema.pre("save", function(next){ if (!this.parametrized) { this.parametrized = v2Parametrize(this.id, this.email); } if (!this.repo_key) { this.repo_key = randomAlphaString(8); } next(); }); userSchema.method("createShareKey", // Instance methods for shared workspace keys function(next?: (err: Err) => void): void { this.share_key = randomAlphaString(48); this.logf().trace("Creating share key", this.consoleText, this.share_key); this.save(next); } ); userSchema.method("removeShareKey", function(next?: (err: Err) => void): void { this.share_key = undefined; this.logf().trace("Removing share key", this.consoleText); this.save(next); } ); userSchema.method("setPassword", // Instance methods for password hashes function(password: string, next?: (err: Err) => void): void { this.logf().trace("Setting password", this.consoleText); if (!password) { process.nextTick(() => { this.password_hash = ""; this.save(next); }); } else { // To create a new password manually, run: // $ node -e "require('bcrypt').hash('foo', 10, console.log)" Bcrypt.hash(password, config.auth.password.salt_rounds, (err, hash) => { this.password_hash = hash; this.save(next); }); } } ); userSchema.method("checkPassword", async function(password: string): Promise { this.logf().trace("Checking password", this.consoleText); if (!this.password_hash || !password) { // Fail if no password is set on user return false; } else { return Bcrypt.compare(password, this.password_hash); } } ); userSchema.method("touchLastActivity", // Other instance methods function(next: (err: Err) => void): void { this.logf().trace("Touching last activity", this.consoleText); this.last_activity = new Date(); this.save(next); } ); userSchema.method("loadInstructorModels", async function(): Promise { let programs = await Promise.all( this.instructor.map(async (program_name: string) => { let program = await Program.findOne({ program_name }); if (!program) { program = new Program(); program.program_name = program_name; } await program.populate("students"); return program; }) ); this._instructorModels = programs; return this; } ); userSchema.method("isFlavorOK", function(flavor: string): boolean { // Note: This function must at least validate that the flavor is valid; to this point, the flavor is unsanitized user input. const availableFlavors = Object.keys(config.flavors); if (availableFlavors.indexOf(flavor) !== -1) { // TODO: Implement this when more interesting logic is available. // return !!this.flavors_enabled; return true; } else if (flavor) { this.logf().trace("WARNING: User requested illegal flavor", flavor); return false; } else { return false; } } ); userSchema.method("logf", function(): ILogger { return logger("user:" + this.id.valueOf()); } ); // Make sure the fields are initialized userSchema.post("init", function(){ if (this.program && this.program !== "default" && !this.share_key) { this.createShareKey(); } }); // JSON representation: include the virtuals (this object will be transmitted // to the actual Octave server) // Leave out the password_hash field to avoid leaking it to the front end. // Also leave out the *_override fields since the information in those fields is available via the corresponding camel-case virtuals. userSchema.set("toJSON", { virtuals: true, transform: function(doc: IUser, ret /* , options */) { delete ret.password_hash; delete ret.tier_override; if (ret.patreon) { delete ret.patreon.oauth2; } delete ret.legal_time_override; delete ret.payload_limit_override; delete ret.countdown_extra_time_override; delete ret.countdown_request_time_override; return ret; } }); export const User = Mongoose.model("User", userSchema); User.on("index", err => { if (err) logger("user-index").error(err); else logger("user-index").info("Init Success"); }); ================================================ FILE: front/src/utils.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import Crypto = require("crypto"); export function emailHash(email: string): string { return "email:" + Crypto.createHash("md5").update(email).digest("hex").substr(0, 12); } export interface IDestroyable { destroyed: boolean; } export interface IWorkspace { sessCode: string|null; destroyD(message: string): void; destroyU(message: string): void; dataD(name: string, val: any): void; dataU(name: string, val: any): void; beginOctaveRequest(flavor: string|null): void; on(event: string, callback: (...args: any[]) => void): void; removeAllListeners(): void; subscribe(): void; unsubscribe(): void; } ================================================ FILE: front/src/views/captcha_error.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/head") %>

ReCAPTCHA Failure

Your request could not be processed due to a ReCAPTCHA Failure. Please click your back button and try again.


If you need help, contact support:

<%- include("partials/foot") %> ================================================ FILE: front/src/views/incorrect_page.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/head") %>

Authentication Failure

You could not be logged in with the email and password that you provided. If you want to create a new account or if you forgot your password, click the button below to be emailed an 11-digit code to <%= query ? query.s : "(empty)" %>.

" />

Once you are signed in, use the "Change Password" option in the menu to set a new password.


If you need help, contact support:

<%- include("partials/foot") %> ================================================ FILE: front/src/views/index.ejs ================================================ <%= t(config.client.title_key, { config }) %> <%- config.ads.head_html %>
<%= t("header.site.adfallback#body") %>
<%- config.ads.abox_html %>

<%= config.client.app_name %>

<%= t("header.site.menu#btn") %>

						
						
">
<% if (config.client.onboarding) { %> <% } %> " href="https://github.com/octave-online/octave-online-server" target="_blank">AGPL Free Software
<% if (config.client.onboarding) { %>

<%= t("promos.welcome.title", { config }) %>

<%= t("promos.welcome.p1", { config }) %>

<%= t("promos.welcome.p2") %>
fminbnd(@(x) x .* log(x), 0, 1) 0.36788
<%= t("promos.welcome.p3") %>
sombrero() plot showing a sombrero
<%= t("promos.welcome.p4") %>
cstr_reactor.m
<% } %>
<% if (config.client.onboarding) { %> <% } %> <% if (buildData.useDistPaths) { %> <% } else { %> <% } %>
================================================ FILE: front/src/views/login_error.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/head") %>

Login Failure

You could not be logged in. This could be because you did not agree to the terms of Google or because you entered an incorrect email login token. Please go back and try again.


If you need help, contact support:

<%- include("partials/foot") %> ================================================ FILE: front/src/views/partials/foot.ejs ================================================
================================================ FILE: front/src/views/partials/head.ejs ================================================ <% if (config.client.theme_collection === "official") { color = "#FF5C4A"; // OO Fire Red } else { color = "#AD928E"; // OO Server Brown } %>
================================================ FILE: front/src/views/patreon_link_error.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/head") %>

Patreon Already Linked

Your Patreon account with ID <%= user_id %> cannot be linked to your Octave Online account "<%= new_email %>" because it is already linked with the one for "<%= old_email %>".


If you need help, contact support:

<%- include("partials/foot") %> ================================================ FILE: front/src/views/token_page.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/head") %>

Email Sign-In

After you sign in, you can use the "Change Password" option in the menu to set a new password. If you don't set a password, you can have Octave Online email you a token whenever you sign in. This prevents you from having to remember a unique password for Octave Online.

The email address you entered was: <%= query ? query.s : "(empty)" %>. If you entered an invalid email address, please click the back button and try again.


If you did not receive your code or need help, contact support:

<%- include("partials/foot") %> ================================================ FILE: front/src/workspace_normal.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import { EventEmitter } from "events"; import Async = require("async"); import { IDestroyable, IWorkspace } from "./utils"; import { IBucket } from "./bucket_model"; import { HydratedUser } from "./user_model"; import { config, newRedisMessenger, logger, ILogger } from "./shared_wrap"; import { octaveHelper, SessionState } from "./octave_session_helper"; type Err = Error|null; const redisMessenger = newRedisMessenger(); export class NormalWorkspace extends EventEmitter implements IWorkspace, IDestroyable { public sessCode: string|null; public destroyed = false; private user: HydratedUser|null; private bucket: IBucket|null; private _log: ILogger; constructor(sessCode: string|null, user: HydratedUser|null, bucket: IBucket|null){ super(); this.sessCode = sessCode; this.user = user; this.bucket = bucket; this._log = logger("workspace-nrm:uninitialized"); process.nextTick(()=>{ this.emit("data", "oo.wsuser", { user }); }); } public destroyD(message: string){ this.destroyed = true; if (!this.sessCode) { return; } // TODO: It's poor style to do a string comparison here if (message !== "Client Disconnect" || config.worker.onDisconnect === "destroy") { this._log.trace("destroyD: Destroy Now:", message); octaveHelper.sendDestroyD(this.sessCode, message); } else if (config.worker.onDisconnect === "ignore") { this._log.trace("destroyD: Ignore:", message); // Ensure that the files are committed immediately, so that if a user reloads the page, they get their data immediately synced this.emit("back", "commit", { comment: "Scripted Commit on Disconnect" }); } else if (config.worker.onDisconnect === "expireShort") { this._log.trace("destroyD: Expire Short:", message); redisMessenger.touchInput(this.sessCode, true); // Ensure that the files are committed immediately, so that if a user reloads the page, they get their data immediately synced this.emit("back", "commit", { comment: "Scripted Commit on Disconnect" }); } } public beginOctaveRequest(flavor: string) { Async.waterfall([ (next: (err: Err, sessCode: string, state: SessionState) => void) => { // Check with Redis about the status of the desired sessCode octaveHelper.getNewSessCode(this.sessCode, next); }, (sessCode: string, state: SessionState, next: (err: Err) => void) => { if (this.destroyed) { if (state !== SessionState.Needed) octaveHelper.sendDestroyD(sessCode, "Client Gone 1"); return; } this.sessCode = sessCode; this._log = logger(`workspace-nrm:${sessCode}`) as ILogger; if (this.user) { this._log.info("User", this.user.consoleText); } if (this.bucket) { this._log.info("Bucket", this.bucket.consoleText); } // Ask for an Octave session if we need one. // Otherwise, inform the client. if (state === SessionState.Needed) { octaveHelper.askForOctave(sessCode, { user: this.user, bucket: this.bucket, flavor }, next); } else { this.emit("sesscode", sessCode); this.emit("data", "prompt", {}); this.emit("data", "files-ready", {}); } }, (next: (err: Err) => void) => { if (this.destroyed) { octaveHelper.sendDestroyD(this.sessCode!, "Client Gone 2"); return; } this.emit("sesscode", this.sessCode!); next(null); } ], (err) => { if (err) this._log.error("REDIS ERROR", err); }); } // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars public destroyU(message: string){ } // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars public dataD(name: string, val: any){ } // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars public dataU(name: string, val: any){ } // eslint-disable-next-line @typescript-eslint/no-empty-function public subscribe() { } // eslint-disable-next-line @typescript-eslint/no-empty-function public unsubscribe() { } } ================================================ FILE: front/src/workspace_shared.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ import { EventEmitter } from "events"; import Crypto = require("crypto"); import Async = require("async"); import Uuid = require("uuid"); import { config, newRedisMessenger, logger, ILogger } from "./shared_wrap"; import { IWorkspace } from "./utils"; import { octaveHelper, SessionState } from "./octave_session_helper"; import { OtDocument } from "./ot_document"; import { User, HydratedUser } from "./user_model"; import { IBucket } from "./bucket_model"; interface BeginOctaveRequestAsyncAuto { user: HydratedUser|null; ready: void; } type Err = Error|null; // Make Redis connections for Shared Workspace const redisMessenger = newRedisMessenger(); const wsSessClient = newRedisMessenger(); wsSessClient.subscribeToWorkspaceMsgs(); wsSessClient.setMaxListeners(30); export class SharedWorkspace extends EventEmitter implements IWorkspace { public wsId: string|null = null; public safeWsId: string|null = null; // ID with no username public sessCode: string|null = null; public destroyed = false; private shareKey: string|null = null; private user: HydratedUser|null = null; private bucket: IBucket|null = null; private docs: { [key: string]: OtDocument } = {}; private msgIds: string[] = []; private otEventCounter = 0; private wsMessageCounter = 0; private statsInterval: any; private touchInterval: any; private _log: ILogger; private logId: string; constructor(shareKey: string|null, user: HydratedUser|null, bucket: IBucket|null, logId: string) { super(); this._log = logger(`workspace-shr:${logId}:uninitialized`); this.logId = logId; this.shareKey = shareKey; this.user = user; this.bucket = bucket; if (bucket) { this.setWsId(`bucket_${bucket.bucket_id}`, false); } else if (user) { this.setWsId(user.parametrized, true); } this.subscribe(); } private setWsId(wsId: string, truncate: boolean) { if (this.wsId) { if (this.wsId !== wsId) this._log.error("SHARED WORKSPACE ERROR: Trying to set wsId to", wsId, "when it was already set to", this.wsId); return; } this.wsId = wsId; // May 2018: remove email-based IDs from log this.safeWsId = truncate ? wsId.substr(0, 8) : wsId; this._log = logger(`workspace-shr:${this.logId}:${this.safeWsId}`); // Create the prompt's OtDocument (every session) // Never emit from constructors since there are no listeners yet; // use process.nextTick() instead const promptId = "prompt." + this.wsId; process.nextTick(() => { this.emit("data", "ws.promptid", promptId); this.docs[promptId] = new OtDocument(promptId, `${this.logId}:prompt.${this.safeWsId}`, ""); this.subscribe(); }); } private forEachDoc(fn: (docId: string,doc: OtDocument) => void){ Object.getOwnPropertyNames(this.docs).forEach((docId) => { fn(docId, this.docs[docId]); }); } public destroyD(message: string){ // The Octave session will be destroyed by expiring keys once all // users have disconnected. There is no need to destroy it here. // Special case: when sharing is disabled or flavor upgraded // TODO: It's poor style to do a string comparison here if (!this.sessCode) { this.destroyed = true; } else if (message === "Sharing Disabled") { this.destroyed = true; octaveHelper.sendDestroyD(this.sessCode, message); } else if (message === "Flavor Upgrade") { // Don't set this.destroyed here because the workspace will get a new sessCode octaveHelper.sendDestroyD(this.sessCode, message); } else { this.destroyed = true; // Ensure that the files are committed immediately, so that if a user reloads the page, they get their data immediately synced this.emit("back", "commit", { comment: "Scripted Commit on Disconnect" }); } } // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars public destroyU(message: string){ } public dataD(name: string, value: any) { if (!name) name = ""; if (!value) value = {}; // NOTE: Remember that each downstream event occurs on just one instance // of IWorkspace, but upstream events occur on ALL listening instances. // Pass OT events down to the OT instances if (name.substr(0,3) === "ot.") { this.forEachDoc(function(docId,doc){ doc.dataD(name, value); }); this.otEventCounter++; return; } // A few special handlers if (name === "save") { // happens when the user saves a file OR creates a new file this.resolveFileSave(value); } // Pass other events into the onUserAction handler this.onUserAction(name, value); } public dataU(name: string, value: any) { if (!name) name = ""; if (!value) value = {}; // NOTE: Remember that each downstream event occurs on just one instance // of IWorkspace, but upstream events occur on ALL listening instances. // A few special handlers if (name === "user") { // happens when the full list of files is read this.resolveFileList(value.files /* , true, value.refresh */); } else if (name === "fileadd") { // happens when SIOFU uploads a file this.resolveFileAdd(value); } else if (name === "renamed") { // happens when a file is successfully renamed this.resolveFileRename(value.oldname, value.newname); } else if (name === "deleted") { // happens when a file is deleted this.resolveFileDelete(value.filename); } } private resolveFileList(files: any){ files = files || {}; for(const filename in files){ if (!files.hasOwnProperty(filename)) continue; const file = files[filename]; if (!file.isText) continue; const content = new Buffer(file.content, "base64").toString(); this.resolveFile(filename, content); } } private resolveFileAdd(file: any){ if (!file.isText) return; this._log.trace("Resolving File Add", file.filename); const content = new Buffer(file.content, "base64").toString(); this.resolveFile(file.filename, content); } private resolveFileSave(file: any){ this._log.trace("Resolving File Save", file.filename); this.resolveFile(file.filename, file.content); } private resolveFile(filename: string, content: string) { // Note: hash.copy() was added in Node.js 13; we could use that here instead of converting via Buffer.from() const hexHash = Crypto.createHash("md5").update(filename).digest("hex"); const shortHash = Buffer.from(hexHash, "hex").toString("base64").replace(/=/g, ""); const docId = `doc.${this.wsId}.${hexHash}`; if (!this.docs[docId]) { this.docs[docId] = new OtDocument(docId, `${this.logId}:doc.${this.safeWsId}.${shortHash}`, content); this.docs[docId].logFilename(filename); this.subscribe(); process.nextTick(() => { this.emit("data", "ws.doc", { docId: docId, filename: filename }); }); } } private resolveFileRename(oldname: string, newname: string) { const oldhash = Crypto.createHash("md5").update(oldname).digest("hex"); const newhash = Crypto.createHash("md5").update(newname).digest("hex"); const oldDocId = "doc." + this.wsId + "." + oldhash; const newDocId = "doc." + this.wsId + "." + newhash; if (!this.docs[oldDocId]) { this._log.trace("Attempted to resolve file rename, but couldn't find old file in shared workspace (non-text file?)", oldname, newname, oldhash); return; } if (this.docs[newDocId]) { this._log.warn("WARNING: Attempted to resolve file rename, but the new name already exists in the workspace", oldname, newname, newhash); return; } this._log.trace("Resolving File Remame", oldname, newname, oldhash, newhash); const doc = this.docs[oldDocId]; delete this.docs[oldDocId]; this.docs[newDocId] = doc; doc.changeDocId(newDocId); this.emit("data", "ws.rename", { oldname: oldname, newname: newname, oldDocId: oldDocId, newDocId: newDocId }); } private resolveFileDelete(filename: string) { const hash = Crypto.createHash("md5").update(filename).digest("hex"); const docId = "doc." + this.wsId + "." + hash; if (!this.docs[docId]) { this._log.trace("Attempted to resolve file delete, but couldn't find file in shared workspace (non-text file?)", filename, hash); return; } this._log.trace("Resolving File Delete", filename); const doc = this.docs[docId]; delete this.docs[docId]; doc.destroy(); this.emit("data", "ws.delete", { filename: filename, docId: docId }); } public beginOctaveRequest(flavor: string) { // Before actually performing the Octave request, ensure that // pre-conditions are satisfied. Async.auto({ user: (next) => { if (this.shareKey && !this.user) { User.findOne({ share_key: this.shareKey }, next); } else { process.nextTick(() => { next(null, this.user); }); } }, ready: ["user", ({user}, next) => { this.user = user; if (user) { // TODO(#41): Make sure this is actually the workspace owner. this.emit("data", "oo.wsuser", { user }); if (!this.wsId) { this.setWsId(user.parametrized, true); this._log.info("Connecting to student:", user.consoleText); } } else if (!this.wsId) { this._log.warn("WARNING: Could not find student with share key", this.shareKey); this.emit("message", "Could not find the specified workspace. Please check your URL and try again."); this.emit("data", "destroy-u", "No Such Workspace"); return; } this.doBeginOctaveRequest(flavor); next(null); }] }, (err) => { if (err) this._log.error("ASYNC ERROR", err); }); } private doBeginOctaveRequest(flavor: string) { Async.waterfall([ (next: (err: Err, sessCode: string) => void) => { // Check if there is a sessCode in Redis already. redisMessenger.getWorkspaceSessCode(this.wsId, next); }, (sessCode: string, next: (err: Err, sessCode: string, state: SessionState) => void) => { if (this.destroyed) return; this.sessCode = sessCode; // Make sure that sessCode is still live. octaveHelper.getNewSessCode(sessCode, next); }, (sessCode: string, state: SessionState, next: (err: Err, result: any) => void) => { if (this.destroyed) return; this._log.trace("SessCode State:", state); // Ask Octave for a session if we need one. if (state === SessionState.Needed) { redisMessenger.setWorkspaceSessCode(this.wsId, sessCode, this.sessCode, next); // Request a file listing if we need one } else if (state === SessionState.Live) { this.emit("sesscode", sessCode); this.emit("data", "prompt", {}); this.emit("data", "files-ready", {}); this.emit("back", "list", {}); // No action necessary } else { this.emit("sesscode", sessCode); } }, (results: any[], next: (err: Err) => void) => { const saved: boolean = results[0]; const sessCode: string = results[1]; if (!saved) return; this.sessCode = sessCode; // Our sessCode was accepted. // Broadcast the new sessCode. redisMessenger.workspaceMsg(this.wsId, "sesscode", sessCode); this.touch(); // Start the new Octave session. this._log.info("Sending Octave Request for Shared Workspace"); octaveHelper.askForOctave(this.sessCode, { user: this.user, bucket: this.bucket, flavor }, next); } ], (err) => { if (err) this._log.error("REDIS ERROR", err); }); } public subscribe() { this.unsubscribe(); wsSessClient.on("ws-sub", this.wsMessageListener); this.touch(); this.touchInterval = setInterval(this.touch, config.redis.expire.interval); this.statsInterval = setInterval(this.recordStats, config.ot.stats_interval); this.forEachDoc((docId, doc) => { doc.subscribe(); doc.on("data", this.onDataO); }); } public unsubscribe() { wsSessClient.removeListener("ws-sub", this.wsMessageListener); clearInterval(this.touchInterval); clearInterval(this.statsInterval); this.forEachDoc((docId, doc) => { doc.unsubscribe(); doc.removeListener("data", this.onDataO); }); } private touch = () => { if (!this.wsId) return; redisMessenger.touchWorkspace(this.wsId); }; //// SHARED WORKSPACE HANDLERS //// private wsMessageListener = (wsId: string, type: string, data: any) => { if (wsId !== this.wsId) return; if (!data) return; this.wsMessageCounter++; let i; switch(type){ case "sesscode": this.emit("sesscode", data); break; case "user-action": i = this.msgIds.indexOf(data.id); if (i > -1) this.msgIds.splice(i, 1); else { this.emit("data", data.name as string, data.data); // Special handlers for a few user actions switch(data.name){ case "ws.save": this.resolveFileSave(data.data); break; } } break; default: break; } }; // This function publishes selected actions into the Redis channel. // Clients on the same channel will resolve those messages in their // "wsMessageListener" function. private onUserAction = (name: string, value: any) => { let eventName, data; switch(name){ case "data": eventName = "ws.command"; data = value.data; break; case "save": eventName = "ws.save"; data = value; break; default: return; } const msgId = Uuid.v4(); this.msgIds.push(msgId); redisMessenger.workspaceMsg(this.wsId, "user-action", { id: msgId, name: eventName, data: data }); }; private onDataO = (name: string, value: any) => { this.emit("data", name, value); }; private recordStats = () => { let opsReceivedTotal = 0, setContentTotal = 0; this.forEachDoc(function(docId,doc){ opsReceivedTotal += doc.opsReceivedCounter; setContentTotal += doc.setContentCounter; }); this._log.debug("STATS:", this.otEventCounter, "OT events and", this.wsMessageCounter, "WS messages and", opsReceivedTotal, "operations received and", setContentTotal, "calls to setContent"); } } ================================================ FILE: front/tsconfig.json ================================================ { "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "dist", /* Redirect output structure to the directory. */ "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } ================================================ FILE: front/typings/easy-no-password.d.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'easy-no-password' { export = easy_no_password; type Err = Error | null; interface Data { stage: number; username: string; token?: string; } interface Options { secret: string; passReqToCallback?: boolean; maxTokenAge?: number; } type VerifiedFn = (err: Err, user?: unknown, info?: any) => void; type VerifyFn = ((username: string, verified: VerifiedFn) => void) | ((req: any, username: string, verified: VerifiedFn) => void); class EasyNoPassword { createToken(username: string, next: (err: Err, token?: string) => void): void; isValid(token: string, username: string, next: (err: Err, isValid?: boolean) => void): void; } function easy_no_password(secret: string, maxTokenAge: number): EasyNoPassword; namespace easy_no_password { export class Strategy { constructor( options: Options, parseRequest: (req: any) => Data | null, sendToken: (username: string, token: string, next: (err: Err) => void) => void, verify: VerifyFn); authenticate(req: any): void; } } } ================================================ FILE: front/typings/i18next-fs-backend.d.ts ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'i18next-fs-backend' { interface IHandleOptions { ignoreRoutes: string[]; } const Backend: import("i18next").Module; export = Backend; } ================================================ FILE: front/typings/i18next-http-middleware.d.ts ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'i18next-http-middleware' { interface IHandleOptions { ignoreRoutes: string[]; } export const LanguageDetector: import("i18next").Module; export function handle(i18next: any, options?: IHandleOptions): import("express").RequestHandler; } ================================================ FILE: front/typings/ot.d.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'ot' { interface ITextOperation { toJSON(): any; } export class Server { constructor(initial_text:string); receiveOperation(rev:number, operation:ITextOperation):ITextOperation; } export class TextOperation { static fromJSON(json: string): ITextOperation; } } ================================================ FILE: front/typings/pseudo-localization.d.ts ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'pseudo-localization' { export function localize(s: string): string; } ================================================ FILE: front/typings/socketio-file-upload.d.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'socketio-file-upload' { export = socketio_file_upload; function socketio_file_upload(options: any): any; namespace socketio_file_upload { const clientPath: string; function listen(app: any): void; function router(req: any, res: any, next: any): void; } } ================================================ FILE: front/typings/socketio-wildcard.d.ts ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ declare module 'socketio-wildcard' { export = socketio_wildcard; function socketio_wildcard(CustomEmitter?: any): any; } ================================================ FILE: package.json ================================================ { "name": "octave-online-server", "version": "1.0.0", "description": "Hosted GNU Octave processes served over the World Wide Web. The infrastructure that powers Octave Online.", "scripts": { "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix" }, "repository": { "type": "git", "url": "git@github.com:octave-online/octave-online-server.git" }, "keywords": [ "matlab", "octave", "online" ], "author": "Octave Online LLC", "license": "AGPL-3.0", "devDependencies": { "@typescript-eslint/eslint-plugin": "^2.13.0", "@typescript-eslint/parser": "^2.13.0", "eslint": "^7.32.0", "typescript": "^3.7.4" } } ================================================ FILE: redirect/README.md ================================================ Octave Online Server: Redirect Service ====================================== This directory contains a standalone service for redirecting shortlinks to the main server. This service is used to power `octav.onl`. ================================================ FILE: redirect/app.js ================================================ /* * Copyright © 2021, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const express = require("express"); const asyncify = require("express-asyncify"); const path = require("path"); const logger = require("morgan"); const config = require("@oo/shared").config; const db = require("./src/db"); const log = require("@oo/shared").logger("app"); const app = asyncify(express()); // view engine setup app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(logger("dev")); app.get("/", async (req, res) => { res.redirect(302, `//${config.front.hostname}:${config.front.port}/`); }); app.get("/:shortlink", async (req, res) => { const shortlink = req.params.shortlink; const bucket = await db.findBucketWithShortlink(shortlink); if (!bucket) { res.status(404); res.render("error", { config, shortlink }); return; } const token = (bucket.butype === "readonly") ? "bucket" : "project"; log.info(`Redirecting ${shortlink} to ${bucket.bucket_id}`); res.redirect(302, `//${config.front.hostname}:${config.front.port}/${token}~${bucket.bucket_id}`); }); module.exports = app; ================================================ FILE: redirect/bin/server.js ================================================ #!/usr/bin/env node /* * Copyright © 2021, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ /** * Module dependencies. */ const app = require("../app"); const db = require("../src/db"); const debug = require("debug")("oo:server"); const http = require("http"); const config = require("@oo/shared").config; (async function() { // Database connection. const mongoUrl = `mongodb://${config.mongo.hostname}:${config.mongo.port}`; const mongoDb = config.mongo.db; await db.connect(mongoUrl, mongoDb); debug("Connected to MongoDB:", mongoUrl, mongoDb); // Get port from environment and store in Express. const port = normalizePort(process.env.PORT || "3000"); app.set("port", port); // Create HTTP server. const server = http.createServer(app); // Listen on provided port, on all network interfaces. server.listen(port); server.on("error", onError); server.on("listening", onListening); // Normalize a port into a number, string, or false. function normalizePort(val) { const port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } // Event listener for HTTP server "error" event. function onError(error) { if (error.syscall !== "listen") { throw error; } const bind = typeof port === "string" ? "Pipe " + port : "Port " + port; // handle specific listen errors with friendly messages switch (error.code) { case "EACCES": debug(bind + " requires elevated privileges"); process.exit(1); break; case "EADDRINUSE": debug(bind + " is already in use"); process.exit(1); break; default: throw error; } } // Event listener for HTTP server "listening" event. function onListening() { const addr = server.address(); const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; debug("Listening on " + bind); } // async function })().catch((err) => { throw err; }); ================================================ FILE: redirect/package.json ================================================ { "name": "@oo/redirect", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/server.js" }, "author": "Octave Online LLC", "license": "AGPL-3.0", "dependencies": { "@oo/shared": "file:../shared", "ejs": "^3.1.6", "express": "^4.17.1", "express-asyncify": "^1.0.1", "mongodb": "^3.6.10", "morgan": "^1.10.0" } } ================================================ FILE: redirect/src/db.js ================================================ /* * Copyright © 2021, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const mongodb = require("mongodb"); let mongoClient = null; let db = null; async function connect(url, dbName) { mongoClient = new mongodb.MongoClient(url); await mongoClient.connect(); db = mongoClient.db(dbName); } async function close() { mongoClient.close(); } async function findBucketWithShortlink(shortlink) { const collection = db.collection("buckets"); const result = await collection.findOne({ shortlink: shortlink }); return result; } module.exports = { connect, close, findBucketWithShortlink, }; ================================================ FILE: redirect/views/error.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <% if (config.client.theme_collection === "official") { color = "#FF5C4A"; // OO Fire Red } else { color = "#AD928E"; // OO Server Brown } %>

Error: Unknown Shortlink: "<%= shortlink %>"

Go to <%= config.front.hostname %>

================================================ FILE: shared/.eslintrc.yml ================================================ ================================================ FILE: shared/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: shared/README.md ================================================ Octave Online Server: Shared Utilities ====================================== This directory contains code that is shared among the client, front server, and back server. When changing any exports here, run `npm run dtx-gen` to re-generate *index.d.ts*, which is used by the front server for TypeScript type definitions. The logging code can optionally send logs to Google Stackdriver. If you don't know what this means, you can ignore this feature. To enable this feature, from this directory, run: ```bash $ npm install ./stackdriver ``` ================================================ FILE: shared/async-cache.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; // This is a helper class that caches an async result (aka Promise) with a certain expiration date. // The getter should be a function that takes a callback function. It should pass the callback function three arguments: err, value, and expires, where expires is how long to keep the value before calling the function again. // bufferTime is subtracted off the expires value when evaluating whether to bust the cache. module.exports = function(getter, bufferTime) { bufferTime = bufferTime || 0; let cachedPromise = null; function makePromise() { let promise = new Promise((resolve, reject) => { getter((err, value, expires) => { if (err) return reject(err); promise.expires = expires; resolve(value); }); }); return promise; } cachedPromise = makePromise(); return function(next) { if (cachedPromise.expires && cachedPromise.expires - new Date() < bufferTime) { cachedPromise = makePromise(); } cachedPromise.then((value) => { next(null, value); }).catch(next); }; }; ================================================ FILE: shared/config-helper.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const config = require("./config"); module.exports = { tier: function(tier) { return Object.assign( { "sessionManager.poolSize": config.sessionManager.poolSize, "sessionManager.poolTier": config.sessionManager.poolTier, "sessionManager.queueBoostTime": config.sessionManager.queueBoostTime, "selinux.cgroup.name": config.selinux.cgroup.name, "selinux.prlimit.addressSpace": config.selinux.prlimit.addressSpace, "session.legalTime.user": config.session.legalTime.user, "session.payloadLimit.user": config.session.payloadLimit.user, "session.countdownExtraTime": config.session.countdownExtraTime, "session.countdownRequestTime": config.session.countdownRequestTime, "session.timewarnTime": config.session.timewarnTime, "session.timeoutTime": config.session.timeoutTime, }, config.tiers[tier] ); }, flavor: function(flavor) { if (!config.flavors[flavor]) { return null; } return Object.assign( {}, config.flavorCommon, config.flavors[flavor] ); } }; ================================================ FILE: shared/config.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const fs = require("fs"); const path = require("path"); const hjson = require("hjson"); const deepmerge = require("deepmerge"); const log = require("./logger")("config"); const defaultConfig = hjson.parse(fs.readFileSync(path.join(__dirname, "..", "config_defaults.hjson")).toString("utf-8")); try { var configBuffer = fs.readFileSync(path.join(__dirname, "..", "config.hjson")); } catch(e) { const message = "Could not read config.hjson. Octave Online Server will use all default settings."; log.warn(message); // Allow console to make sure the message always gets printed, even when not in debug mode: // eslint-disable-next-line no-console console.error("Notice: " + message); configBuffer = Buffer.alloc(0); } try { var config = hjson.parse(configBuffer.toString("utf-8")); } catch(e) { // The process will die here if config.hjson is not found, so console is OK // eslint-disable-next-line no-console console.error("Possible syntax error in config file!"); // eslint-disable-next-line no-console console.error(e); process.exit(1); } const resolvedConfig = deepmerge(defaultConfig, config); module.exports = resolvedConfig; ================================================ FILE: shared/gcp/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: shared/gcp/fetch_translations.js ================================================ #!/usr/bin/env node /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); const util = require("util"); const execFile = util.promisify(require("child_process").execFile); const gcp = require("./index"); const { config, logger } = require("@oo/shared"); const log = logger("fetch_translations"); module.exports = async function fetchTranslations(buildData) { const tmpdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "oo-translations-")); log.trace("Made tmpdir:", tmpdir); const targzPath = path.join(tmpdir, "i18next_locales.tar.gz"); log.trace("Making request to Google Cloud Storage to download translations…"); await gcp.downloadFile(log.log, config.gcp.artifacts_bucket, config.gcp.i18next_locales_tar_gz, targzPath); log.trace("Translations succesfully downloaded"); const { stdout, stderr } = await execFile("tar", ["zxf", "i18next_locales.tar.gz"], { cwd: tmpdir }); log.trace("Unpacked i18next_locales.tar.gz", stdout, stderr); const i18nextLocalesDir = path.join(tmpdir, "i18next_locales"); buildData.locales_path = path.join(i18nextLocalesDir, "{{lng}}.json"); buildData.locales = []; const re = new RegExp("^(\\w+)\\.json"); const filenames = await fs.promises.readdir(i18nextLocalesDir); log.trace("filenames present in unpacked tar.gz:", filenames); for (let filename of filenames) { let match = re.exec(filename); if (match) { buildData.locales.push(match[1]); } } }; ================================================ FILE: shared/gcp/index.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const path = require("path"); const Compute = require("@google-cloud/compute"); const { Storage } = require("@google-cloud/storage"); const gcpMetadata = require("gcp-metadata"); const { config, gitarchive } = require("@oo/shared"); ///// Compute ///// let _computeClient = null; function getComputeClient() { if (!_computeClient) { if (config.gcp.credentials) { _computeClient = new Compute({ projectId: config.gcp.credentials.project_id, credentials: config.gcp.credentials }); } else { _computeClient = new Compute(); } } return _computeClient; } async function getRecommendedSize(log) { const client = getComputeClient() .zone(config.gcp.zone) .autoscaler(config.gcp.instance_group_name); const [, result ] = await client.get(); log("Recommended size:", result.recommendedSize); return result.recommendedSize; } async function getTargetSize(log) { const client = getComputeClient() .zone(config.gcp.zone) .instanceGroupManager(config.gcp.instance_group_name); const [, result ] = await client.get(); log("Target size:", result.targetSize); return result.targetSize; } async function getAutoscalerInfo(log) { const [ recommendedSize, targetSize ] = await Promise.all([ getRecommendedSize(log), getTargetSize(log) ]); return { recommendedSize, targetSize }; } async function getSelfName(log) { const name = await gcpMetadata.instance("name"); log("Self name:", name); return name; } async function removeSelfFromGroup(log) { const selfName = await getSelfName(log); const vm = getComputeClient() .zone(config.gcp.zone) .vm(selfName); const client = getComputeClient() .zone(config.gcp.zone) .instanceGroupManager(config.gcp.instance_group_name); let operation; if (config.gcp.instance_group_removal_method === "abandon") { log("Abandoning self"); [ operation ] = await client.abandonInstances(vm); } else if (config.gcp.instance_group_removal_method === "delete") { log("Deleting self"); [ operation ] = await client.deleteInstances(vm); } else { throw new Error("Unknown removal method"); } return operation; } ///// Storage ///// let _storageClient = null; function getStorageClient() { if (!_storageClient) { if (config.gcp.credentials) { _storageClient = new Storage({ projectId: config.gcp.credentials.project_id, credentials: config.gcp.credentials }); } else { _storageClient = new Storage(); } } return _storageClient; } async function downloadFile(log, bucketName, srcPath, destination) { const client = getStorageClient() .bucket(bucketName); await client.file(srcPath).download({ destination }); log(`gs://${bucketName}/${srcPath} downloaded to ${destination}.`); } async function uploadRepoArchive(log, tld, name) { const needsArchive = await gitarchive.repoContainsRefs(tld, name); if (!needsArchive) { log(`Empty or nonexistant repo: ${tld}/${name}`); return; } const bucketName = config.gcp.archive_bucket; await doUploadRepo(log, tld, name, bucketName); } async function uploadRepoSnapshot(log, tld, name) { const bucketName = config.gcp.snapshots_bucket; const file = await doUploadRepo(log, tld, name, bucketName); return (await file.getSignedUrl({ version: "v4", action: "read", expires: Date.now() + config.gcp.snapshots_duration, }))[0]; } async function doUploadRepo(log, tld, name, bucketName) { const filename = gitarchive.generateFilename(name); const bucketPath = `zips/${tld}/${filename}`; log(`Streaming to gs://${bucketName}/${bucketPath}`); const file = getStorageClient() .bucket(bucketName) .file(bucketPath); const stream = file.createWriteStream({ resumable: false, }); await gitarchive.createRepoSnapshot(tld, name, stream); return file; } async function restoreRepoFromCloudStorage(log, tld, name, bucketName, bucketPath) { log(`Fetching gs://${bucketName}/${bucketPath}`); const file = getStorageClient() .bucket(bucketName) .file(bucketPath); const fileContents = (await file.download())[0]; log("Zip file downloaded: # of bytes:", fileContents.length); const branchName = path.basename(bucketPath).replace(/:/g, ""); await gitarchive.restoreRepoFromZipFile(log, tld, name, branchName, fileContents); } module.exports = { getAutoscalerInfo, removeSelfFromGroup, downloadFile, uploadRepoArchive, uploadRepoSnapshot, restoreRepoFromCloudStorage, }; ================================================ FILE: shared/gcp/package.json ================================================ { "name": "@oo/shared_gcp", "version": "0.0.0", "description": "GCP bindings for Octave Online", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Shane F. Carr", "license": "AGPL-3.0", "dependencies": { "@google-cloud/compute": "^2.1.0", "@google-cloud/storage": "^5.3.0", "@oo/shared": "file:..", "gcp-metadata": "^4.3.0" } } ================================================ FILE: shared/gcp/reboot_or_remove_self.js ================================================ #!/usr/bin/env node /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; /* eslint-disable no-console */ const child_process = require("child_process"); const util = require("util"); const gcp = require("./index"); const log = console.log; const execFile = util.promisify(child_process.execFile); async function reboot() { return execFile("sudo", ["/usr/sbin/reboot"], { stdio: "inherit" }); } async function main() { const { recommendedSize, targetSize } = await gcp.getAutoscalerInfo(log); log("Recommended/Target Size:", recommendedSize, targetSize); if (targetSize > recommendedSize) { log("Removing self from group"); return await gcp.removeSelfFromGroup(log); } else { log("Requesting reboot"); return await reboot(); } } module.exports = function() { main().then((results) => { log(results); process.exit(0); }).catch((err) => { console.error(err); process.exit(1); }); }; ================================================ FILE: shared/gitarchive.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const child_process = require("child_process"); const fsPromises = require("fs").promises; const jszip = require("jszip"); const os = require("os"); const path = require("path"); const util = require("util"); const execFilePromise = util.promisify(child_process.execFile); const config = require("./config"); const log = require("./logger")("gitarchive"); async function repoContainsRefs(tld, name) { const remote = `git://${config.git.hostname}:${config.git.gitDaemonPort}/${tld}/${name}.git`; let stdoutLen = 0; const p = child_process.spawn("git", ["ls-remote", remote, "master"]); p.stdout.on("data", (data) => { stdoutLen += data.length; }); p.stderr.on("data", (data) => { log.log(data); }); return new Promise(function(resolve, reject) { p.on("close", (code) => { if (code) { log.trace("Git exited with code " + code); resolve(false); } else { log.trace("Number of bytes from stdout:", stdoutLen); resolve(!!stdoutLen); } }); p.on("error", reject); }); } async function createRepoSnapshot(tld, name, outStream) { const remote = `git://${config.git.hostname}:${config.git.gitDaemonPort}/${tld}/${name}.git`; log.info(`Archiving ${remote}`); const p = child_process.spawn("git", ["archive", "--format=zip", "--remote="+remote, "master"]); p.stdout.pipe(outStream); p.stderr.on("data", (data) => { log.log(data); }); return new Promise(function(resolve, reject) { p.on("close", (code) => { if (code) { log.error("Git exited with code " + code); } resolve(); }); p.on("error", reject); }); } async function restoreRepoFromZipFile(log, tld, name, branchName, zipFileBlob) { const remote = `git://${config.git.hostname}:${config.git.gitDaemonPort}/${tld}/${name}.git`; const zip = await jszip.loadAsync(zipFileBlob, { createFolders: true }); const tmpdir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "oo-reporestore-")); const gitdir = path.join(tmpdir, "work"); const gitoptbase = ["-c", "user.email='webmaster@octave-online.net'", "-c", "user.name='Octave Online'"]; log("tmpdir:", tmpdir); try { log("A-clone", await execFilePromise("git", ["clone", remote, "work"], { cwd: tmpdir })); log("A-commit-0", await execFilePromise("git", gitoptbase.concat(["commit", "--allow-empty", "-m", "Prep for restoration"]), { cwd: gitdir })); log("A-co-orphan", await execFilePromise("git", ["checkout", "--no-guess", "--orphan", branchName], { cwd: gitdir })); log("A-git-rm", await new Promise((resolve, reject) => { child_process.execFile("git", ["rm", "-rf", "."], { cwd: gitdir }, function(err, stdout, stderr) { // Errors are expected here if the repo is empty resolve({ stdout, stderr }); }); })); for (let [relativePath, file] of Object.entries(zip.files)) { const fullPath = path.join(gitdir, relativePath); if (file.dir) { log("Creating directory:", relativePath); await fsPromises.mkdir(fullPath); } else { log("Writing file:", relativePath); const fileData = await zip.file(relativePath).async("nodebuffer"); // TODO: Should we restore "date", "unixPermissions", ... ? await fsPromises.writeFile(fullPath, fileData); } } log("A-add-1", await execFilePromise("git", ["add", "-A"], { cwd: gitdir })); log("A-commit-1", await execFilePromise("git", gitoptbase.concat(["commit", "-m", "Snapshot: " + branchName]), { cwd: gitdir })); log("A-checkout-master", await execFilePromise("git", ["checkout", "master"], { cwd: gitdir })); const mergeOutput = await new Promise((resolve, reject) => { child_process.execFile("git", gitoptbase.concat(["merge", "--allow-unrelated-histories", "--squash", "--no-commit", branchName]), { cwd: gitdir }, function(err, stdout, stderr) { // Errors are expected here if there was a merge conflict; ignore them gracefully. resolve({ stdout, stderr }); }); }); log("A-merge", mergeOutput); log("A-add-2", await execFilePromise("git", ["add", "-A"], { cwd: gitdir })); log("A-commit-2", await execFilePromise("git", gitoptbase.concat(["commit", "-m", "Restoring: " + branchName + "\n\nGit Merge Output:\n-----\n" + mergeOutput.stdout]), { cwd: gitdir })); log("A-push", await execFilePromise("git", ["push", "origin", "master"], { cwd: gitdir })); } catch (e) { throw e; } finally { log("C1", await execFilePromise("rm", ["-rf", tmpdir])); } } function generateFilename(name) { return `oo_${new Date().toISOString()}_${name}.zip`; } module.exports = { repoContainsRefs, createRepoSnapshot, generateFilename, restoreRepoFromZipFile, }; ================================================ FILE: shared/hostname.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const child_process = require("child_process"); let hostnameCache = null; module.exports = function getHostname() { if (!hostnameCache) { hostnameCache = child_process.execSync("hostname").toString("utf8").trim(); } return hostnameCache; }; ================================================ FILE: shared/index.d.ts ================================================ /** Declaration file generated by dts-gen */ export class JSONStreamSafe { constructor(...args: any[]); static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } export class OnlineOffline { constructor(...args: any[]); create(...args: any[]): void; destroy(...args: any[]): void; isOnline(...args: any[]): void; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } export class Queue { constructor(...args: any[]); dequeue(...args: any[]): void; enqueue(...args: any[]): void; isEmpty(...args: any[]): void; peek(...args: any[]): void; removeAll(...args: any[]): void; size(...args: any[]): void; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } export class RedisMessenger { constructor(...args: any[]); applyOtOperation(...args: any[]): void; changeOtDocId(...args: any[]): void; close(...args: any[]): void; destroyD(...args: any[]): void; destroyOtDoc(...args: any[]): void; destroyU(...args: any[]): void; enableOtScriptsSync(...args: any[]): void; enableSessCodeScriptsSync(...args: any[]): void; getSessCode(...args: any[]): void; getSessCodeFlavor(...args: any[]): void; getWorkspaceSessCode(...args: any[]): void; input(...args: any[]): void; isValid(...args: any[]): void; loadOtDoc(...args: any[]): void; otMsg(...args: any[]): void; output(...args: any[]): void; putSessCode(...args: any[]): void; putSessCodeFlavor(...args: any[]): void; replyToFlavorStatus(...args: any[]): void; replyToRebootRequest(...args: any[]): void; requestFlavorStatus(...args: any[]): void; requestReboot(...args: any[]): void; setLive(...args: any[]): void; setOtDocContent(...args: any[]): void; setWorkspaceSessCode(...args: any[]): void; subscribeToDestroyD(...args: any[]): void; subscribeToDestroyU(...args: any[]): void; subscribeToExpired(...args: any[]): void; subscribeToFlavorStatus(...args: any[]): void; subscribeToInput(...args: any[]): void; subscribeToOtMsgs(...args: any[]): void; subscribeToOutput(...args: any[]): void; subscribeToRebootRequests(...args: any[]): void; subscribeToWorkspaceMsgs(...args: any[]): void; touchInput(...args: any[]): void; touchOtDoc(...args: any[]): void; touchOutput(...args: any[]): void; touchWorkspace(...args: any[]): void; workspaceMsg(...args: any[]): void; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } export class RedisQueue { constructor(...args: any[]); enqueueMessage(...args: any[]): void; reset(...args: any[]): void; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } export class StdioMessenger { constructor(...args: any[]); sendMessage(...args: any[]): void; setReadStream(...args: any[]): void; setWriteStream(...args: any[]): void; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } export const config: { ads: { abox_html: string; disabled: boolean; head_html: string; }; auth: { easy: { max_token_age: number; secret: string; }; google: { oauth_key: string; oauth_secret: string; }; password: { delay: number; salt_rounds: number; }; utils_admin: { users: { webmaster: string; }; }; }; client: { announcement_display: string; announcement_html: string; app_name: string; description_key: string; gacode: string; gtagid: string; onboarding: boolean; theme_collection: string; theme_color: string; title_key: string; uservoice: string; welcome_back_ms: number; }; docker: { cpuShares: number; cwd: string; diskQuotaKiB: number; gitdir: string; images: { filesystemSuffix: string; octaveSuffix: string; }; memoryShares: string; }; email: { from: string; productName: string; provider: string; supportUrl: string; }; flavorCommon: { blockVolume: boolean; defaultClusterSize: number; idleTime: number; image_uuid: string; network_uuid: string; statusInterval: number; }; flavors: { basic: { blockVolume: boolean; rackspaceFlavor: string; }; memory: { rackspaceFlavor: string; }; }; front: { cookie: { max_age: number; name: string; secret: string; }; flavor_log_interval: number; hostname: string; listen_port: number; locales: string[]; locales_path: string; port: number; protocol: string; require_https: boolean; socket_io_path: string; static_path: string; view_cache_clear_interval: number; }; gcp: { archive_bucket: string; artifacts_bucket: string; credentials: { auth_provider_x509_cert_url: string; auth_uri: string; client_email: string; client_id: string; client_x509_cert_url: string; private_key: string; private_key_id: string; project_id: string; token_uri: string; type: string; }; health_check_port: number; i18next_locales_tar_gz: string; instance_group_name: string; instance_group_removal_method: string; snapshots_bucket: string; snapshots_duration: number; zone: string; }; git: { author: { email: string; name: string; }; autoCommitInterval: number; commitTimeLimit: number; createRepoPort: number; gitDaemonPort: number; hostname: string; httpUrl: string; supportsAllowUnrelatedHistories: boolean; }; gith: { hostname: string; }; mailgun: { api_key: string; domain: string; }; maintenance: { interval: number; maxNodesInMaintenance: number; minNodesInCluster: number; pauseDuration: number; requestInterval: number; responseWaitTime: number; }; mongo: { db: string; hostname: string; port: number; }; ot: { document_expire: { interval: number; timeout: number; }; operation_expire: number; stats_interval: number; }; patreon: { client_id: string; client_secret: string; login_redirect: string; redirect_url: string; state_max_token_age: number; state_secret: string; tiers: { "4941716": { name: string; oo_tier: string; }; "4941717": { name: string; oo_tier: string; }; "4941718": { name: string; oo_tier: string; }; "4994534": { name: string; oo_tier: string; }; }; webhook_secret: string; }; postmark: { onDemandSnapshots: { stream: string; template: string; }; serverToken: string; templateAlias: string; }; rackspace: { api_key: string; identity_base_url: string; personality_filename: string; servers_base_url: string; username: string; }; recaptcha: { secretKey: string; siteKey: string; }; redirect: { hostname: string; }; redis: { expire: { interval: number; timeout: number; timeoutShort: number; }; hostname: string; maxPayload: number; options: { auth_pass: string; }; port: number; }; selinux: { cgroup: { conf: string; cpuPeriod: number; cpuQuota: number; gid: string; name: string; uid: string; }; prlimit: { addressSpace: number; }; }; session: { countdownExtraTime: number; countdownRequestTime: number; countdownRequestTimeBuffer: number; implementation: string; jsonMaxMessageLength: number; legalTime: { guest: number; user: number; }; payloadAcknowledgeDelay: number; payloadLimit: { guest: number; user: number; }; payloadMessageDelay: number; textFileSizeLimit: number; timeoutTime: number; timewarnMessage: string; timewarnTime: number; urlreadMaxBytes: number; urlreadPatterns: string[]; }; sessionManager: { logInterval: number; poolInterval: number; poolSize: number; poolTier: boolean; queueBoostTime: number; startupTimeLimit: number; }; statsd: { hostname: string; port: number; }; tiers: { root: { "ads.disabled": boolean; "sessionManager.poolTier": string; "sessionManager.queueBoostTime": number; }; vip: { "ads.disabled": boolean; "selinux.cgroup.name": string; "selinux.prlimit.addressSpace": number; "session.countdownExtraTime": number; "session.countdownRequestTime": number; "session.legalTime.user": number; "session.payloadLimit.user": number; "session.timeoutTime": number; "session.timewarnTime": number; "sessionManager.poolSize": number; "sessionManager.queueBoostTime": number; }; }; worker: { clockInterval: { max: number; min: number; }; clockStrategy: string; logDir: string; maxSessions: number; monitorLogs: { subdir: string; }; onDisconnect: string; sessionLogs: { depth: number; subdir: string; }; token: string; uid: number; }; }; export function asyncCache(getter: any, bufferTime: any): any; export function hostname(): any; export function logger(id: any): any; export function onceMessage(emitter: any, messageName: any, next: any): void; export function silent(messageRegex: any, next: any, ...args: any[]): any; export function timeLimit(milliseconds: any, defaults: any, callback: any, ...args: any[]): any; export namespace JSONStreamSafe { class EventEmitter { constructor(opts: any); addListener(type: any, listener: any): any; emit(type: any, args: any): any; eventNames(): any; getMaxListeners(): any; listenerCount(type: any): any; listeners(type: any): any; off(type: any, listener: any): any; on(type: any, listener: any): any; once(type: any, listener: any): any; prependListener(type: any, listener: any): any; prependOnceListener(type: any, listener: any): any; rawListeners(type: any): any; removeAllListeners(type: any, ...args: any[]): any; removeListener(type: any, listener: any): any; setMaxListeners(n: any): any; static EventEmitter: any; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } } export namespace OnlineOffline { class EventEmitter { constructor(opts: any); addListener(type: any, listener: any): any; emit(type: any, args: any): any; eventNames(): any; getMaxListeners(): any; listenerCount(type: any): any; listeners(type: any): any; off(type: any, listener: any): any; on(type: any, listener: any): any; once(type: any, listener: any): any; prependListener(type: any, listener: any): any; prependOnceListener(type: any, listener: any): any; rawListeners(type: any): any; removeAllListeners(type: any, ...args: any[]): any; removeListener(type: any, listener: any): any; setMaxListeners(n: any): any; static EventEmitter: any; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } } export namespace Queue { class EventEmitter { constructor(opts: any); addListener(type: any, listener: any): any; emit(type: any, args: any): any; eventNames(): any; getMaxListeners(): any; listenerCount(type: any): any; listeners(type: any): any; off(type: any, listener: any): any; on(type: any, listener: any): any; once(type: any, listener: any): any; prependListener(type: any, listener: any): any; prependOnceListener(type: any, listener: any): any; rawListeners(type: any): any; removeAllListeners(type: any, ...args: any[]): any; removeListener(type: any, listener: any): any; setMaxListeners(n: any): any; static EventEmitter: any; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } } export namespace RedisMessenger { class EventEmitter { constructor(opts: any); addListener(type: any, listener: any): any; emit(type: any, args: any): any; eventNames(): any; getMaxListeners(): any; listenerCount(type: any): any; listeners(type: any): any; off(type: any, listener: any): any; on(type: any, listener: any): any; once(type: any, listener: any): any; prependListener(type: any, listener: any): any; prependOnceListener(type: any, listener: any): any; rawListeners(type: any): any; removeAllListeners(type: any, ...args: any[]): any; removeListener(type: any, listener: any): any; setMaxListeners(n: any): any; static EventEmitter: any; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } } export namespace RedisQueue { class EventEmitter { constructor(opts: any); addListener(type: any, listener: any): any; emit(type: any, args: any): any; eventNames(): any; getMaxListeners(): any; listenerCount(type: any): any; listeners(type: any): any; off(type: any, listener: any): any; on(type: any, listener: any): any; once(type: any, listener: any): any; prependListener(type: any, listener: any): any; prependOnceListener(type: any, listener: any): any; rawListeners(type: any): any; removeAllListeners(type: any, ...args: any[]): any; removeListener(type: any, listener: any): any; setMaxListeners(n: any): any; static EventEmitter: any; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } } export namespace StdioMessenger { class EventEmitter { constructor(opts: any); addListener(type: any, listener: any): any; emit(type: any, args: any): any; eventNames(): any; getMaxListeners(): any; listenerCount(type: any): any; listeners(type: any): any; off(type: any, listener: any): any; on(type: any, listener: any): any; once(type: any, listener: any): any; prependListener(type: any, listener: any): any; prependOnceListener(type: any, listener: any): any; rawListeners(type: any): any; removeAllListeners(type: any, ...args: any[]): any; removeListener(type: any, listener: any): any; setMaxListeners(n: any): any; static EventEmitter: any; static captureRejectionSymbol: any; static captureRejections: boolean; static defaultMaxListeners: number; static errorMonitor: any; static getEventListeners(emitterOrTarget: any, type: any): any; static init(opts: any): void; static kMaxEventTargetListeners: any; static kMaxEventTargetListenersWarned: any; static listenerCount(emitter: any, type: any): any; static on(emitter: any, event: any, options: any): any; static once(emitter: any, name: any, options: any): any; static setMaxListeners(n: any, eventTargets: any): void; static usingDomains: boolean; } } export namespace config2 { function flavor(flavor: any): any; function tier(tier: any): any; } export namespace gitarchive { function createRepoSnapshot(tld: any, name: any, outStream: any): any; function generateFilename(name: any): any; function repoContainsRefs(tld: any, name: any): any; function restoreRepoFromZipFile(log: any, tld: any, name: any, branchName: any, zipFileBlob: any): void; } export namespace metrics { function gauge(id: any, value: any): any; } export namespace redisUtil { function createClient(): any; function getSessCodeFromChannel(channel: any): any; function isValidSessCode(sessCode: any): any; namespace chan { const destroyD: string; const destroyU: string; const needsOctave: string; const rebootRequest: string; function attachment(id: any): any; function flavorStatus(flavor: any): any; function input(sessCode: any): any; function needsOctaveFlavor(flavor: any): any; function otCnt(docId: any): any; function otDoc(docId: any): any; function otOps(docId: any): any; function otSub(docId: any): any; function output(sessCode: any): any; function session(sessCode: any): any; function wsSess(wsId: any): any; function wsSub(wsId: any): any; } } ================================================ FILE: shared/index.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; module.exports = { asyncCache: require("./async-cache"), config: require("./config"), config2: require("./config-helper"), gitarchive: require("./gitarchive"), hostname: require("./hostname"), JSONStreamSafe: require("./json-stream-safe"), logger: require("./logger"), metrics: require("./metrics"), onceMessage: require("./once-message"), OnlineOffline: require("./online-offline"), Queue: require("./queue"), RedisMessenger: require("./redis-messenger"), RedisQueue: require("./redis-queue"), redisUtil: require("./redis-util"), silent: require("./silent"), StdioMessenger: require("./stdio-messenger"), timeLimit: require("./time-limit"), }; ================================================ FILE: shared/json-stream-safe.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const parserFactory = require("stream-json").parser; const streamValues = require("stream-json/streamers/StreamValues").streamValues; const EventEmitter = require("events"); const log = require("./logger")("json-stream-safe"); // This is a thin wrapper around JSONStream that recovers from parse errors. class JSONStreamSafe extends EventEmitter { constructor(stream) { super(); this.stream = stream; this._run(); } _run() { // Note: upon error events, we must restart the parser, because Node closes streams upon any error event. this.stream .pipe(parserFactory({ jsonStreaming: true })) .on("error", (err) => { log.error("JSON syntax error", err); this._run(); }) .pipe(streamValues()) .on("error", (err) => { log.error("Could not unpack value", err); this._run(); }) .on("data", (d) => { this.emit("data", d.value); }) .on("end", () => { // TODO: Does this method get run? Is it important? this.emit("end"); }); } } module.exports = JSONStreamSafe; ================================================ FILE: shared/logger.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; // Centralized logger definition for most OO projects. let writeStackdriverLog = function() {}; // Use debug-logger with all logs going to stderr const debugLogger = require("debug-logger").config({ levels: { trace: { fd: 2 }, debug: { fd: 2 }, log: { fd: 2 }, info: { fd: 2 }, warn: { fd: 2 }, error: { fd: 2 } } }); try { const stackdriver = require("./stackdriver"); writeStackdriverLog = stackdriver.writeLog; debugLogger("oo:logger").info("Note: Stackdriver logging enabled"); } catch(err) { // Don't log to stackdriver debugLogger("oo:logger").info("Note: Stackdriver logging disabled"); } process.on("unhandledRejection", (reason, promise) => { debugLogger("oo:unhandledRejection").error("Unhandled Rejection at:", promise, "reason:", reason); writeStackdriverLog("error", "unhandledRejection", [promise, reason]); }); module.exports = function(id) { const impl = debugLogger("oo:" + id); return { /** Trace: low-level operational details */ trace: (...args) => { impl.trace(...args); // don't log to stackdriver }, /** Debug: information related to app health */ debug: (...args) => { impl.debug(...args); writeStackdriverLog("debug", id, args); }, /** Log: uncategorized messages from another source */ log: (...args) => { impl.log(...args); // don't log to stackdriver }, /** Info: changes to an application state */ info: (...args) => { impl.info(...args); writeStackdriverLog("info", id, args); }, /** Warn: unusual state, but not an error */ warn: (...args) => { impl.warn(...args); writeStackdriverLog("warning", id, args); }, /** Error: unexpected state */ error: (...args) => { impl.error(...args); writeStackdriverLog("error", id, args); }, }; }; ================================================ FILE: shared/lua/get-sesscode.lua ================================================ -- Copyright © 2018, Octave Online LLC -- -- This file is part of Octave Online Server. -- -- Octave Online Server is free software: you can redistribute it and/or -- modify it under the terms of the GNU Affero General Public License as -- published by the Free Software Foundation, either version 3 of the License, -- or (at your option) any later version. -- -- Octave Online Server is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public -- License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with Octave Online Server. If not, see -- . local needs_octave_key = KEYS[1] local token = ARGV[1] -- TODO: An additional key is used in this script beyond the keys passed in from the arguments. Could cause issue with clusters. local sesscodes = redis.call("ZRANGE", needs_octave_key, 0, 0) while table.getn(sesscodes) == 1 do local sesscode = sesscodes[1] redis.call("ZREM", needs_octave_key, sesscode) -- Check that the sesscode is still valid; if its hash was deleted, then we should discard this sesscode. local sess_info_key = "oo:session:" .. sesscode local exists = redis.call("EXISTS", sess_info_key) if exists == 1 then local user = redis.call("HGET", sess_info_key, "user") redis.call("HSET", sess_info_key, "worker", token) return {sesscode, user} else -- Try again; loop back to the top sesscodes = redis.call("ZRANGE", needs_octave_key, 0, 0) end end return -1 ================================================ FILE: shared/lua/ot.lua ================================================ -- Copyright © 2018, Octave Online LLC -- -- This file is part of Octave Online Server. -- -- Octave Online Server is free software: you can redistribute it and/or -- modify it under the terms of the GNU Affero General Public License as -- published by the Free Software Foundation, either version 3 of the License, -- or (at your option) any later version. -- -- Octave Online Server is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public -- License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with Octave Online Server. If not, see -- . ------------------------------------------------------------------------------- -- This code is heavily based on the JavaScript implementation ot.js: -- https://github.com/Operational-Transformation/ot.js/ -- -- Copyright © 2012-2014 Tim Baumann, http://timbaumann.info -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the “Software”), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. ------------------------------------------------------------------------------- -- Perform Operational Transormation. -- Lua has poor support for UTF-8, so we need to have custom functions. -- These are based on those originally written by Cosmin Apreutesei: -- https://github.com/luapower/utf8 -- Use the prefix "lutf8" in case at some point in the future Redis adds the Lua 5.3 built-in "utf8" library. -- Byte index of the next char after the char at byte index i, followed by a valid flag for the char at byte index i. -- nil if not found. invalid characters are iterated as 1-byte chars. function lutf8_next(s, i) if not i then if #s == 0 then return nil end return 1, true --fake flag (doesn't matter since this flag is not to be taken as full validation) end if i > #s then return end local c = s:byte(i) if c >= 0x00 and c <= 0x7F then i = i + 1 elseif c >= 0xC2 and c <= 0xDF then i = i + 2 elseif c >= 0xE0 and c <= 0xEF then i = i + 3 elseif c >= 0xF0 and c <= 0xF4 then i = i + 4 else --invalid return i + 1, false end if i > #s then return end return i, true end -- Iterator over byte indices in the string. function lutf8_byte_indices(s, previ) return lutf8_next, s, previ end -- Returns the number of UTF-8 characters in the string, like string.len. function lutf8_len(s) local len = 0 for _ in lutf8_byte_indices(s) do len = len + 1 end return len end -- Performs a substring operation, like string.sub. -- TODO: This is an O(N) operation, which makes merging changes together O(N^2) in the worst case. See if there's a way to make this operation more efficient. function lutf8_sub(s, start_ci, end_ci) local ci = 0 local start_i = 1 local end_i = s:len() for i in lutf8_byte_indices(s) do ci = ci + 1 if ci == start_ci then start_i = i end if ci == end_ci+1 then end_i = i-1 break end end return string.sub(s, start_i, end_i) end -- Remove redundant ops from an operations table function condense(ops) local i = 2 while ops[i] ~= nil do -- insert/insert if type(ops[i]) == "string" and type(ops[i-1]) == "string" then ops[i-1] = ops[i-1] .. ops[i] table.remove(ops, i) -- delete/insert -- The order of these operations does not matter with -- respect to the "apply" function, but for consistency -- we always put "insert" first. elseif type(ops[i]) == "string" and ops[i-1]<0 then local tmp = ops[i] ops[i] = ops[i-1] ops[i-1] = tmp -- go backwards in case the new insert can be condensed i = i-1 -- other/insert (do nothing) elseif type(ops[i]) == "string" or type(ops[i-1]) == "string" then i = i+1 -- delete/delete elseif ops[i]<0 and ops[i-1]<0 then ops[i-1] = ops[i-1] + ops[i] table.remove(ops, i) -- retain/retain elseif ops[i]>0 and ops[i-1]>0 then ops[i-1] = ops[i-1] + ops[i] table.remove(ops, i) -- something else that we can't condense else i = i+1 end if i<2 then i = 2 end end end -- Transform takes two operations A and B that happened -- concurrently and produces two operations A' and B' -- such that apply(apply(S,A),B') == apply(apply(S,B),A') -- This function is the heart of OT. function transform(ops1, ops2) local ops1p = {} local ops2p = {} local i1 = 1 local i2 = 1 local op1 = ops1[i1] local op2 = ops2[i2] local minl = 0 while op1 ~= nil or op2 ~= nil do -- insert by player 1 -- break tie by prefering player 1 if type(op1) == "string" then table.insert(ops1p, op1) -- insert table.insert(ops2p, lutf8_len(op1)) -- retain i1 = i1+1; op1 = ops1[i1] -- insert by player 2 elseif type(op2) == "string" then table.insert(ops1p, lutf8_len(op2)) -- retain table.insert(ops2p, op2) -- insert i2 = i2+1; op2 = ops2[i2] -- retain/retain elseif op1>0 and op2>0 then if op1>op2 then minl = op2 op1 = op1 - op2 i2 = i2+1; op2 = ops2[i2] elseif op1 == op2 then minl = op2 i1 = i1+1; op1 = ops1[i1] i2 = i2+1; op2 = ops2[i2] else minl = op1 op2 = op2 - op1 i1 = i1+1; op1 = ops1[i1] end table.insert(ops1p, minl) -- retain table.insert(ops2p, minl) -- retain -- delete/delete elseif op1<0 and op2<0 then if op1 < op2 then op1 = op1 - op2 i2 = i2+1; op2 = ops2[i2] elseif op1 == op2 then i1 = i1+1; op1 = ops1[i1] i2 = i2+1; op2 = ops2[i2] else op2 = op2 - op1 i1 = i1+1; op1 = ops1[i1] end -- delete/retain elseif op1<0 and op2>0 then if -op1 > op2 then minl = op2 op1 = op1 + op2 i2 = i2+1; op2 = ops2[i2] elseif -op1 == op2 then minl = op2 i1 = i1+1; op1 = ops1[i1] i2 = i2+1; op2 = ops2[i2] else minl = -op1 op2 = op2 + op1 i1 = i1+1; op1 = ops1[i1] end table.insert(ops1p, -minl) -- delete -- retain/delete elseif op1>0 and op2<0 then if op1 > -op2 then minl = -op2 op1 = op1 + op2 i2 = i2+1; op2 = ops2[i2] elseif op1 == -op2 then minl = op1 i1 = i1+1; op1 = ops1[i1] i2 = i2+1; op2 = ops2[i2] else minl = op1 op2 = op2 + op1 i1 = i1+1; op1 = ops1[i1] end table.insert(ops2p, -minl) -- delete -- noop elseif op1==0 then i1 = i1+1; op1 = ops1[i1] elseif op2==0 then i2 = i2+1; op2 = ops2[i2] -- unknown else error() end end condense(ops1p) condense(ops2p) return ops1p, ops2p end -- Apply an operation to a text function apply(str, ops) local j = 1 local res = "" for i,op in pairs(ops) do -- insert if type(op)=="string" then res = res .. op -- delete elseif op<0 then j = j - op -- retain else res = res .. lutf8_sub(str, j, j+op-1) j = j + op end end return res end ================================================ FILE: shared/lua/ot_apply.lua ================================================ -- Copyright © 2018, Octave Online LLC -- -- This file is part of Octave Online Server. -- -- Octave Online Server is free software: you can redistribute it and/or -- modify it under the terms of the GNU Affero General Public License as -- published by the Free Software Foundation, either version 3 of the License, -- or (at your option) any later version. -- -- Octave Online Server is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public -- License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with Octave Online Server. If not, see -- . -- Perform operations on server. -- Read arguments local rev = tonumber(ARGV[1]) local message = cjson.decode(ARGV[2]) local op_expiretime = tonumber(ARGV[3]) local doc_expiretime = tonumber(ARGV[4]) local ops_key = KEYS[1] local doc_key = KEYS[2] local sub_key = KEYS[3] local cnt_key = KEYS[4] -- Load concurrent operations. The operations store is truncated according -- to the call to "EXPIRE" later in this file, so we need to compute the index -- into the operations store relative to the current length of the store. If -- that index is out of range of the list, then some of the concurrent -- operations required for transforming the new operation have been expired -- out of the cache, and we need to raise an error. local nrev = redis.call("GET", cnt_key) if not nrev then nrev = 0 end local nstore = redis.call("LLEN", ops_key) if not nstore then nstore = 0 end local idx = nstore-nrev+rev if idx < 0 then error("Operation history is too shallow") end local concurrent = redis.call("LRANGE", ops_key, idx, -1) -- Transform the new operation against all the concurrent operations if concurrent then for i,cops in pairs(concurrent) do message.ops = transform(message.ops, cjson.decode(cops)) end end -- Save the operation redis.call("RPUSH", ops_key, cjson.encode(message.ops)) redis.call("INCR", cnt_key) -- Load and apply to the document local doc = redis.call("GET", doc_key) if type(doc)=="boolean" then doc="" end doc = apply(doc, message.ops) redis.call("SET", doc_key, doc) -- Touch the operation keys' expire value redis.call("EXPIRE", ops_key, op_expiretime) redis.call("EXPIRE", doc_key, doc_expiretime) redis.call("EXPIRE", cnt_key, doc_expiretime) -- Publish to the subscribe channel redis.call("PUBLISH", sub_key, cjson.encode(message)) ================================================ FILE: shared/lua/ot_set.lua ================================================ -- Copyright © 2018, Octave Online LLC -- -- This file is part of Octave Online Server. -- -- Octave Online Server is free software: you can redistribute it and/or -- modify it under the terms of the GNU Affero General Public License as -- published by the Free Software Foundation, either version 3 of the License, -- or (at your option) any later version. -- -- Octave Online Server is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public -- License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with Octave Online Server. If not, see -- . -- Set an OT document to the specified value using transformations. -- Read arguments local content = ARGV[1] local message = cjson.decode(ARGV[2]) local op_expiretime = tonumber(ARGV[3]) local doc_expiretime = tonumber(ARGV[4]) local ops_key = KEYS[1] local doc_key = KEYS[2] local sub_key = KEYS[3] local cnt_key = KEYS[4] local old_content = redis.call("GET", doc_key) if type(old_content)=="boolean" then -- Document doesn't exist. Make an operation to set its content. message.ops = {content} else -- No action necessary return 0 end -- Update Redis redis.call("SET", doc_key, content) redis.call("SET", cnt_key, 0) -- Touch the operation keys' expire value redis.call("EXPIRE", ops_key, op_expiretime) redis.call("EXPIRE", doc_key, doc_expiretime) redis.call("EXPIRE", cnt_key, doc_expiretime) return 1 ================================================ FILE: shared/lua/ot_test.lua ================================================ -- Copyright © 2018, Octave Online LLC -- -- This file is part of Octave Online Server. -- -- Octave Online Server is free software: you can redistribute it and/or -- modify it under the terms of the GNU Affero General Public License as -- published by the Free Software Foundation, either version 3 of the License, -- or (at your option) any later version. -- -- Octave Online Server is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public -- License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with Octave Online Server. If not, see -- . -- Tests for the OT implementation. require "ot" function table_equals(a,b) i = 1 while a[i] ~= nil do if a[i] ~= b[i] then return false end i = i+1 end if a[i]~=nil or b[i]~=nil then return false end return true end -- Test a few condense operations ops = {5, "hello", -3, 6} condense(ops) assert(table_equals(ops, {5, "hello", -3, 6})) ops = {5, -3, "hello", 6} condense(ops) assert(table_equals(ops, {5, "hello", -3, 6})) ops = {2, 3, "he", -2, "llo", -1, 4, 2} condense(ops) assert(table_equals(ops, {5, "hello", -3, 6})) -- Test some apply operations str = apply("hello world", {6, "earth", -5}) assert(str == "hello earth") ops1 = {6, "world", -5} -- lorem world ops2 = {"hello", -5, 6} -- hello ipsum str = apply(apply("lorem ipsum", ops1), ops2) assert(str == "hello world") -- Test a transform operation str = "lorem ipsum" ops1 = {6, "world", -5} -- lorem world ops2 = {"hello", -5, 6} -- hello ipsum ops1p, ops2p = transform(ops1, ops2) -- hello world str1 = apply(apply(str, ops1), ops2p) str2 = apply(apply(str, ops2), ops1p) assert(str1 == str2) -- Test some UTF-8 commands str = "abçd" assert(lutf8_len(str) == 4) assert(lutf8_sub(str, 2, 3) == "bç") ================================================ FILE: shared/metrics.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const lynx = require("lynx"); const config = require("./config"); let _client = null; function getClient() { if (!_client) { _client = new lynx(config.statsd.hostname, config.statsd.port); } return _client; } module.exports = { gauge: function(id, value) { return getClient().gauge(id, value); } }; ================================================ FILE: shared/once-message.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; // This is a simple utility function that waits for a specific message name before continuing. module.exports = function onceMessage(emitter, messageName, next) { const messageCallback = (name, content) => { if (name === messageName) { next(null, content); emitter.removeListener("message", messageCallback); } }; emitter.on("message", messageCallback); }; ================================================ FILE: shared/online-offline.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); const Queue = require("./queue"); // const log = require("./logger")("online-offline"); /** * This is a class that handles safely creating and destroying something asynchronously. * Possible states: INIT, CREATING, ONLINE, PENDING-DESTROY, DESTROYING, and DESTROYED. * Required methods to implement in derived classes: _doCreate, _doDestroy. * Derived classes may read the state but should never change it. */ class OnlineOffline extends EventEmitter { constructor() { super(); this._state = "INIT"; this._createCBs = new Queue(); this._destroyCBs = new Queue(); } isOnline() { return this._state === "ONLINE"; } create(next) { next = next || function(){}; switch (this._state) { case "INIT": { this._state = "CREATING"; this._createCBs.enqueue(next); let args = Array.prototype.slice.call(arguments, 1); args.unshift(this._afterCreate.bind(this)); this._doCreate.apply(this, args); break; } case "CREATING": case "PENDING-DESTROY": this._createCBs.enqueue(next); break; case "ONLINE": return process.nextTick(() => { next(new Error("Already created")); }); case "DESTROYING": case "DESTROYED": return process.nextTick(() => { next(new Error("Already destroyed")); }); default: this._log.error("Unknown state:", this._state); return process.nextTick(() => { next(new Error("Internal online-offline error")); }); } } _afterCreate(err) { switch (this._state) { case "CREATING": this._state = "ONLINE"; if (err) { this.destroy(); } break; case "PENDING-DESTROY": this._state = "ONLINE"; this.destroy(); if (!err) { err = new Error("Pending destroy"); } break; case "DESTROYED": if (!err) { err = new Error("Already destroyed"); } break; case "INIT": case "ONLINE": case "DESTROYING": this._log.error("Unexpected state:", this._state); break; default: this._log.error("Unknown state:", this._state); break; } let args = Array.prototype.slice.call(arguments, 1); args.unshift(err); while (!this._createCBs.isEmpty()) { let cb = this._createCBs.dequeue(); process.nextTick(() => { cb.apply(this, args); }); } } destroy(next) { next = next || function(){}; switch (this._state) { case "ONLINE": { this._state = "DESTROYING"; this._destroyCBs.enqueue(next); let args = Array.prototype.slice.call(arguments, 1); args.unshift(this._afterDestroy.bind(this)); this._doDestroy.apply(this, args); break; } case "CREATING": case "PENDING-DESTROY": this._state = "PENDING-DESTROY"; this._destroyCBs.enqueue(next); break; case "DESTROYING": this._destroyCBs.enqueue(next); break; case "INIT": case "DESTROYED": this._state = "DESTROYED"; return process.nextTick(() => { next(null); }); default: this._log.error("Unknown state:", this._state); return process.nextTick(() => { next(new Error("Internal online-offline error")); }); } } _afterDestroy(err) { switch (this._state) { case "DESTROYING": this._state = "DESTROYED"; break; case "INIT": case "CREATING": case "ONLINE": case "PENDING-DESTROY": case "DESTROYED": this._log.error("Unexpected state:", this._state); break; default: this._log.error("Unknown state:", this._state); break; } while (!this._destroyCBs.isEmpty()) { let cb = this._destroyCBs.dequeue(); process.nextTick(() => { cb.call(this, err); }); } } _internalDestroyed(err) { switch (this._state) { case "DESTROYING": case "CREATING": case "ONLINE": case "PENDING-DESTROY": this._state = "DESTROYING"; this._afterDestroy(err); break; case "DESTROYED": break; case "INIT": this._log.error("Unexpected state:", this._state); break; default: this._log.error("Unknown state:", this._state); break; } } } module.exports = OnlineOffline; ================================================ FILE: shared/package.json ================================================ { "name": "@oo/shared", "version": "0.0.0", "description": "Utility routines shared by one or more projects", "main": "index.js", "scripts": { "dts-gen": "dts-gen -m '@oo/shared' -f index.d.ts --overwrite" }, "author": "Octave Online LLC", "license": "AGPL-3.0", "private": true, "engines": { "node": "18.x" }, "dependencies": { "async": "^1.5.2", "debug-logger": "^0.4.1", "deepmerge": "^2.1.1", "hjson": "^3.2.2", "jszip": "^3.7.1", "lynx": "^0.2.0", "redis": "^3.1.2", "redis-scripto2": "^0.2", "stream-json": "^1.3.1", "uuid": "^3.3.2" }, "devDependencies": { "@oo/shared": "file:../shared", "dts-gen": "^0.6.1" } } ================================================ FILE: shared/queue.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); // This is a simple queue implementation that emits an event whenever an item is enqueued. class Queue extends EventEmitter { constructor() { super(); this.enabled = true; this._items = []; } size() { return this._items.length; } enqueue(item) { if (this.enabled) { this._items.push(item); this.emit("enqueue"); } } dequeue() { if (this._items.length > 0) { return this._items.shift(); } else { throw new RangeError("Can't dequeue from an empty queue"); } } peek() { if (this._items.length > 0) { return this._items[0]; } else { throw new RangeError("Can't peek into an empty queue"); } } removeAll() { this._items = []; } isEmpty() { return this._items.length === 0; } } module.exports = Queue; ================================================ FILE: shared/redis-messenger.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); const logger = require("./logger"); const redisUtil = require("./redis-util"); const Scripto = require("redis-scripto2"); const path = require("path"); const uuid = require("uuid"); const fs = require("fs"); const config = require("./config"); const hostname = require("./hostname")(); class RedisMessenger extends EventEmitter { constructor() { super(); this._client = redisUtil.createClient(); this._subscribed = false; this._scriptManager = null; this.id = uuid.v4().substr(0, 8); // For logging this._log = logger("redis-messenger:" + this.id); this._mlog = logger("redis-messenger:" + this.id + ":minor"); this._client.on("error", (err) => { this._log.trace("REDIS CLIENT", err); }); this._client.on("end", () => { this._log.trace("Redis connection ended"); }); this._client.on("reconnecting", (info) => { this._log.trace("Redis reconnecting:", info); this._log.debug("FYI: Subscription set:", this._client.subscription_set); }); this._client.on("ready", (info) => { this._log.trace("Redis ready:", info); }); } // PUBLIC METHODS enableSessCodeScriptsSync() { this._makeScriptManager(); this._scriptManager.loadFromFile("get-sesscode", path.join(__dirname, "lua/get-sesscode.lua")); return this; } enableOtScriptsSync() { this._makeScriptManager(); let otApplyScript = fs.readFileSync(path.join(__dirname, "lua/ot.lua"), "utf8"); otApplyScript += fs.readFileSync(path.join(__dirname, "lua/ot_apply.lua"), "utf8"); otApplyScript = otApplyScript.replace(/function/g, "local function"); let otSetScript = fs.readFileSync(path.join(__dirname, "lua/ot_set.lua"), "utf8"); this._scriptManager.load({ "ot-apply": otApplyScript, "ot-set": otSetScript }); return this; } _makeScriptManager() { if (!this._scriptManager) { this._scriptManager = new Scripto(this._client); } } input(sessCode, name, content) { this._ensureNotSubscribed(); let channel = redisUtil.chan.input(sessCode); let messageString = this._serializeMessage(name, content); this._client.publish(channel, messageString); } subscribeToInput() { this._psubscribe(redisUtil.chan.input); this.on("_message", this._emitMessage.bind(this)); return this; } output(sessCode, name, content) { this._ensureNotSubscribed(); let channel = redisUtil.chan.output(sessCode); let messageString = this._serializeMessage(name, content); this._client.publish(channel, messageString); } subscribeToOutput() { this._psubscribe(redisUtil.chan.output); this.on("_message", this._emitMessage.bind(this)); return this; } putSessCode(sessCode, millisecondBoost, content) { return this._putSessCode(sessCode, millisecondBoost, redisUtil.chan.needsOctave, content); } putSessCodeFlavor(sessCode, millisecondBoost, flavor, content) { return this._putSessCode(sessCode, millisecondBoost, redisUtil.chan.needsOctaveFlavor(flavor), content); } _putSessCode(sessCode, millisecondBoost, channel, content) { this._ensureNotSubscribed(); let time = new Date().valueOf() - millisecondBoost/1000; let multi = this._client.multi(); multi.zadd(channel, time, sessCode); // NOTE: For backwards compatibilty, this field is called "user" instead of "content" multi.hset(redisUtil.chan.session(sessCode), "user", JSON.stringify(content)); multi.hset(redisUtil.chan.session(sessCode), "live", "false"); multi.set(redisUtil.chan.input(sessCode), time); multi.set(redisUtil.chan.output(sessCode), time); multi.exec(this._handleError.bind(this)); } getSessCode(next) { return this._getSessCode(redisUtil.chan.needsOctave, next); } getSessCodeFlavor(flavor, next) { return this._getSessCode(redisUtil.chan.needsOctaveFlavor(flavor), next); } _getSessCode(channel, next) { this._runScript("get-sesscode", [channel], [config.worker.token], (err, result) => { if (err) this._handleError(err); if (result === -1) return next(null, null, null); try { let content = JSON.parse(result[1]); this.touchOutput(result[0]); next(null, result[0], content); } catch (err) { next(err, null, null); } }); } destroyD(sessCode, reason) { this._ensureNotSubscribed(); let channel = redisUtil.chan.destroyD; let message = { sessCode, message: reason }; let multi = this._client.multi(); multi.del(redisUtil.chan.session(sessCode)); multi.del(redisUtil.chan.input(sessCode)); multi.del(redisUtil.chan.output(sessCode)); // For efficiency, zrem the key from needsOctave. However, the key could be in a needs-flavor channel. That case is handled in get-sesscode.lua. multi.zrem(redisUtil.chan.needsOctave, sessCode); multi.publish(channel, JSON.stringify(message)); multi.exec(this._handleError.bind(this)); } subscribeToDestroyD() { this._subscribe(redisUtil.chan.destroyD); this.on("_message", (message) => { this.emit("destroy-d", message.sessCode, message.message); }); return this; } destroyU(sessCode, reason) { this._ensureNotSubscribed(); let channel = redisUtil.chan.destroyU; let message = { sessCode, message: reason }; let multi = this._client.multi(); multi.del(redisUtil.chan.session(sessCode)); multi.del(redisUtil.chan.input(sessCode)); multi.del(redisUtil.chan.output(sessCode)); multi.publish(channel, JSON.stringify(message)); multi.exec(this._handleError.bind(this)); } subscribeToDestroyU() { this._subscribe(redisUtil.chan.destroyU); this.on("_message", (message) => { this.emit("destroy-u", message.sessCode, message.message); }); return this; } setLive(sessCode) { this._ensureNotSubscribed(); this._client.hset(redisUtil.chan.session(sessCode), "live", "true"); this.touchOutput(sessCode); } isValid(sessCode, next) { this._ensureNotSubscribed(); this._client.hget(redisUtil.chan.session(sessCode), "live", next); } touchInput(sessCode, short) { this._ensureNotSubscribed(); const timeout = short ? config.redis.expire.timeoutShort : config.redis.expire.timeout; const multi = this._client.multi(); multi.expire(redisUtil.chan.session(sessCode), timeout/1000); multi.expire(redisUtil.chan.input(sessCode), timeout/1000); multi.exec(this._handleError.bind(this)); } touchOutput(sessCode) { this._ensureNotSubscribed(); const multi = this._client.multi(); multi.expire(redisUtil.chan.session(sessCode), config.redis.expire.timeout/1000); multi.expire(redisUtil.chan.output(sessCode), config.redis.expire.timeout/1000); multi.exec(this._handleError.bind(this)); } touchWorkspace(wsId) { this._ensureNotSubscribed(); // TODO: Should these "expire" calls on intervals be buffered and sent to the server in batches, both here and above? this._client.expire(redisUtil.chan.wsSess(wsId), config.redis.expire.timeout/1000, this._handleError.bind(this)); } touchOtDoc(docId) { this._ensureNotSubscribed(); const multi = this._client.multi(); multi.expire(redisUtil.chan.otCnt(docId), config.ot.document_expire.timeout/1000); multi.expire(redisUtil.chan.otDoc(docId), config.ot.document_expire.timeout/1000); multi.exec(this._handleError.bind(this)); } subscribeToExpired() { this._epsubscribe(); this.on("_message", (sessCode, channel) => { this.emit("expired", sessCode, channel); }); return this; } requestReboot(id, priority) { return this._requestReboot(redisUtil.chan.rebootRequest, id, priority); } requestFlavorStatus(flavor, id, priority) { return this._requestReboot(redisUtil.chan.flavorStatus(flavor), id, priority); } _requestReboot(channel, id, priority) { this._ensureNotSubscribed(); let message = { id, isRequest: true, token: config.worker.token, hostname, priority }; this._client.publish(channel, JSON.stringify(message), this._handleError.bind(this)); } replyToRebootRequest(id, response) { return this._replyToRebootRequest(redisUtil.chan.rebootRequest, id, response); } replyToFlavorStatus(flavor, id, response) { return this._replyToRebootRequest(redisUtil.chan.flavorStatus(flavor), id, response); } _replyToRebootRequest(channel, id, response) { this._ensureNotSubscribed(); let message = { id, isRequest: false, token: config.worker.token, hostname, response }; this._client.publish(channel, JSON.stringify(message), this._handleError.bind(this)); } subscribeToRebootRequests() { return this._subscribeToRebootRequests(redisUtil.chan.rebootRequest, "reboot-request"); } subscribeToFlavorStatus(flavor) { return this._subscribeToRebootRequests(redisUtil.chan.flavorStatus(flavor), "flavor-status"); } _subscribeToRebootRequests(channel, eventName) { this._subscribe(channel); this.on("_message", (message) => { this.emit(eventName, message.id, message.isRequest, message); }); return this; } workspaceMsg(wsId, type, data) { this._ensureNotSubscribed(); let channel = redisUtil.chan.wsSub(wsId); let messageString = JSON.stringify({ type, data }); this._client.publish(channel, messageString, this._handleError.bind(this)); } subscribeToWorkspaceMsgs() { this._psubscribe(redisUtil.chan.wsSub); this.on("_message", (wsId, message) => { this.emit("ws-sub", wsId, message.type, message.data); }); return this; } getWorkspaceSessCode(wsId, next) { this._ensureNotSubscribed(); this._client.get(redisUtil.chan.wsSess(wsId), next); } setWorkspaceSessCode(wsId, newSessCode, oldSessCode, next) { this._ensureNotSubscribed(); // Perform a Compare-And-Swap operation (this is oddly not // in core Redis, so a Lua script is required) const casScript = "local k=redis.call(\"GET\",KEYS[1]); print(k); if k==false or k==ARGV[2] then redis.call(\"SET\",KEYS[1],ARGV[1]); return {true,ARGV[1]}; end; return {false,k};"; this._client.eval(casScript, 1, redisUtil.chan.wsSess(wsId), newSessCode, oldSessCode || "-", next); } otMsg(docId, obj) { this._ensureNotSubscribed(); const channel = redisUtil.chan.otSub(docId); const messageString = JSON.stringify(obj); this._client.publish(channel, messageString, this._handleError.bind(this)); } subscribeToOtMsgs() { this._psubscribe(redisUtil.chan.otSub); this.on("_message", (docId, obj) => { this.emit("ot-sub", docId, obj); }); return this; } changeOtDocId(oldDocId, newDocId) { this._ensureNotSubscribed(); const multi = this._client.multi(); multi.rename(redisUtil.chan.otOps(oldDocId), redisUtil.chan.otOps(newDocId)); multi.rename(redisUtil.chan.otDoc(oldDocId), redisUtil.chan.otDoc(newDocId)); multi.rename(redisUtil.chan.otSub(oldDocId), redisUtil.chan.otSub(newDocId)); multi.rename(redisUtil.chan.otCnt(oldDocId), redisUtil.chan.otCnt(newDocId)); multi.exec(this._handleError.bind(this)); } destroyOtDoc(docId) { this._ensureNotSubscribed(); const multi = this._client.multi(); multi.del(redisUtil.chan.otOps(docId)); multi.del(redisUtil.chan.otDoc(docId)); multi.del(redisUtil.chan.otSub(docId)); multi.del(redisUtil.chan.otCnt(docId)); multi.exec(this._handleError.bind(this)); } loadOtDoc(docId, next) { const multi = this._client.multi(); multi.get(redisUtil.chan.otCnt(docId)); multi.get(redisUtil.chan.otDoc(docId)); multi.exec((err, res) => { if (err) return next(err); const rev = Number(res[0]) || -1; const content = res[1] || ""; next(err, rev, content); }); } applyOtOperation(docId, rev, message) { const ops_key = redisUtil.chan.otOps(docId); const doc_key = redisUtil.chan.otDoc(docId); const sub_key = redisUtil.chan.otSub(docId); const cnt_key = redisUtil.chan.otCnt(docId); this._runScript( "ot-apply", [ops_key, doc_key, sub_key, cnt_key], [ rev, JSON.stringify(message), config.ot.operation_expire/1000, config.ot.document_expire.timeout/1000 ], this._handleError.bind(this)); } setOtDocContent(docId, content, message) { const ops_key = redisUtil.chan.otOps(docId); const doc_key = redisUtil.chan.otDoc(docId); const sub_key = redisUtil.chan.otSub(docId); const cnt_key = redisUtil.chan.otCnt(docId); this._runScript( "ot-set", [ops_key, doc_key, sub_key, cnt_key], [ content, JSON.stringify(message), config.ot.operation_expire/1000, config.ot.document_expire.timeout/1000, ], this._handleError.bind(this)); } close() { this._client.end(true); } // PRIVATE METHODS _subscribe(channel) { this._ensureNotSubscribed(); this._subscribed = true; this._log.trace("Subscribing to channel:", channel); this._client.subscribe(channel); this._client.on("message", (channel, message) => { try { let obj = JSON.parse(message); this.emit("_message", obj); } catch (err) { this._handleError(err); } }); return this; } _psubscribe(chanFn) { this._ensureNotSubscribed(); this._subscribed = true; const pattern = chanFn("*"); this._log.trace("Subscribing to pattern:", pattern); const regex = new RegExp(`^${chanFn("([^:]+)")}$`); this._client.psubscribe(pattern); this._client.on("pmessage", (pattern, channel, message) => { const match = regex.exec(channel); if (!match) { this._log.error("pmessage result does not match regex", pattern, channel); return; } const sessCode = match[1]; try { let obj = JSON.parse(message); this.emit("_message", sessCode, obj); } catch (err) { this._handleError(err); } }); return this; } _epsubscribe() { this._ensureNotSubscribed(); this._subscribed = true; this._log.trace("Subscribing to expiring keys"); this._client.subscribe("__keyevent@0__:expired"); this._client.on("message", (channel, message) => { this._mlog.trace("Received expire message", channel, message); const match = /^oo:\w+:(\w+)$/.exec(message); if (match) { this._log.trace("Matched sesscode in expire message"); this.emit("_message", match[1], message); } }); } _runScript(memo, keys, args, next) { this._ensureNotSubscribed(); if (!this._scriptManager) throw new Error("Need to enable scripts first"); this._mlog.trace("Running script:", memo); this._scriptManager.run(memo, keys, args, next); } _serializeMessage(name, content) { // Protect against name length if (name.length > config.redis.maxPayload) { this._log.error(new Error("Name length exceeds max redis payload length!")); return null; } // If data is too long, save it as an "attachment" let contentString = JSON.stringify(content); if (contentString.length > config.redis.maxPayload) { let id = uuid.v4(); this._mlog.trace("Sending content as attachment:", name, id, contentString.length); this._uploadAttachment(id, contentString, this._handleError.bind(this)); return JSON.stringify({ name, attachment: id }); } // The message is short enough to send as one chunk! this._mlog.trace("Sending content on channel:", name); return JSON.stringify({ name, data: content }); } _emitMessage(sessCode, message) { let getData = (next) => { if (message.data) return process.nextTick(() => { next(null, message.data); }); else { return this._downloadAttachment(message.attachment, (err, contentString) => { this._mlog.trace("Received content as attachment:", message.name, message.attachment, contentString.length); try { next(err, JSON.parse(contentString)); } catch (_err) { next(_err); } }); } }; this.emit("message", sessCode, message.name, getData); } _uploadAttachment(id, contentString, next) { let channel = redisUtil.chan.attachment(id); // Create a new client to offload bandwidth from the main artery channel let client = redisUtil.createClient(); client.on("error", this._handleError.bind(this)); // Upload the attachment along with an expire time let multi = client.multi(); multi.lpush(channel, contentString); multi.expire(channel, config.redis.expire.timeout/1000); multi.exec((err) => { client.quit(); next(err); }); } _downloadAttachment(id, next) { let channel = redisUtil.chan.attachment(id); // Create a new client to offload bandwidth from the main artery channel let client = redisUtil.createClient(); client.on("error", this._handleError.bind(this)); // Download the attachment client.brpoplpush(channel, channel, config.redis.expire.timeout/1000, (err, response) => { client.quit(); if (response) { next(err, response); } else { next(err, JSON.stringify(null)); } }); } _handleError() { if (arguments[0]) this._log.warn.apply(this, arguments); } _ensureNotSubscribed() { if (this._subscribed) throw new Error("Can't call this method on a client that is subscribed to a channel"); } } module.exports = RedisMessenger; ================================================ FILE: shared/redis-queue.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); const logger = require("./logger"); const Queue = require("./queue"); // For streams of messages with attachments, use a queue to handle incoming messages to ensure that messages are processed in order. The loading of data via attachments is asynchronous and may cause messages to change order. class RedisQueue extends EventEmitter { constructor(logId) { super(); this._queue = new Queue(); this._log = logger("rq:" + logId); this._mlog = logger("rq:" + logId + ":minor"); } enqueueMessage(name, getData) { let message = { name, ready: false }; this._queue.enqueue(message); getData((err, content) => { if (err) this._log.error(err); message.content = content; message.ready = true; this._flushMessageQueue(); }); } reset() { this._queue.removeAll(); } _flushMessageQueue() { while (!this._queue.isEmpty() && this._queue.peek().ready) { let message = this._queue.dequeue(); this._mlog.trace("Emitting message:", message.name); this.emit("message", message.name, message.content); } this._mlog.debug("Items remaining in queue:", this._queue.size()); } } module.exports = RedisQueue; ================================================ FILE: shared/redis-util.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const EventEmitter = require("events"); const redis = require("redis"); const log = require("./logger")("redis-util"); const mlog = require("./logger")("redis-util:minor"); const config = require("./config"); // const log = require("./logger")("redis-util"); const PORT = config.redis.port; const HOSTNAME = config.redis.hostname; const OPTIONS = config.redis.options; class MockRedisClient extends EventEmitter { psubscribe(name, ...args) { mlog.trace("Ignoring psubscribe:", name); } subscribe(name, ...args) { mlog.trace("Ignoring subscribe:", name); } multi() { mlog.trace("Ignoring multi"); return new MockRedisClient(); } send_command(name, ...args) { mlog.trace("Ignoring send_command:", name); } del(name, ...args) { mlog.trace("Ignoring del:", name); } publish(name, ...args) { mlog.trace("Ignoring publish:", name); } zadd(name, ...args) { mlog.trace("Ignoring zadd:", name); } zrem(name, ...args) { mlog.trace("Ignoring zrem:", name); } hget(name, ...args) { mlog.trace("Ignoring hget:", name); } hset(name, ...args) { mlog.trace("Ignoring hset:", name); } set(name, ...args) { mlog.trace("Ignoring set:", name); } expire(name, ...args) { mlog.trace("Ignoring expire:", name); } exec() { mlog.trace("Ignoring exec"); } } let createClient; if (config.redis.hostname) { createClient = () => { mlog.debug("Connecting to Redis"); return redis.createClient(PORT, HOSTNAME, OPTIONS); }; } else { log.warn("Redis is disabled; using mock"); createClient = () => { mlog.debug("Creating mock Redis client"); return new MockRedisClient(); } } module.exports = { createClient, chan: { needsOctave: "oo:needs-octave", destroyD: "oo:destroy-d", destroyU: "oo:destroy-u", rebootRequest: "oo:reboot-request", session: (sessCode) => { return "oo:session:" + sessCode; }, input: (sessCode) => { return "oo:input:" + sessCode; }, output: (sessCode) => { return "oo:output:" + sessCode; }, attachment: (id) => { return "attachment:" + id; }, needsOctaveFlavor: (flavor) => { return "oo:needs-flavor:" + flavor; }, flavorStatus: (flavor) => { return "oo:flavor-status-" + flavor; }, otOps: (docId) => { return "ot:" + docId + ":ops"; }, otDoc: (docId) => { return "ot:" + docId + ":doc"; }, otSub: (docId) => { return "ot:" + docId + ":sub"; }, otCnt: (docId) => { return "ot:" + docId + ":cnt"; }, wsSess: (wsId) => { return "oo:workspace:" + wsId + ":sess"; }, wsSub: (wsId) => { return "oo:workspace:" + wsId + ":sub"; }, }, getSessCodeFromChannel: (channel) => { const match = /^oo:(\w+):(\w+)$/.exec(channel); if (!match) throw new Error("Can't extract sessCode from channel name"); return match[2]; }, isValidSessCode: (sessCode) => { return /^\w{24}$/.test(sessCode); } }; ================================================ FILE: shared/silent.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ var mlog = require("./logger")("silent:minor"); // Callback wrapper that catches errors and prevents them from propagating. function silent(messageRegex, next) { function pass() { var args = Array.prototype.slice.call(arguments, 1); mlog.log("Suppressed additional output (regex: " + messageRegex + "): ", JSON.stringify(args)); args.unshift(null); next.apply(this, args); } function logNext() { var args = Array.prototype.slice.call(arguments, 1); mlog.log("Pass-through additional output (regex: " + messageRegex + "): ", JSON.stringify(args)); next.apply(this, arguments); } // The following function needs to be an ES5-style function in order for "arguments" to work. Note: At the time of writing, the ES6 spread operator is not supported in Node.JS. function checkError() { var err = arguments[0]; if (err && messageRegex.test(err.message)) { mlog.trace("Message suppressed (regex: " + messageRegex + ")"); return pass.apply(this, arguments); } else { return logNext.apply(this, arguments); } } function checkStdout(err, stdout /*, stderr*/) { if (stdout && messageRegex.test(stdout)) { mlog.trace("Message suppressed from stdout (regex: " + messageRegex + ")"); return pass.apply(this, arguments); } else { return checkError.apply(this, arguments); } } checkError.stdout = checkStdout; return checkError; } module.exports = silent; ================================================ FILE: shared/stackdriver/index.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const util = require("util"); const { ErrorReporting } = require("@google-cloud/error-reporting"); const { Logging } = require("@google-cloud/logging"); const errors = new ErrorReporting(); // TODO: Allow each project to customize this? let gcpLog = new Logging().log("oo-projects"); function reportError(label, message) { const errorEvent = errors.event() .setMessage(message) .setUser(label); errors.report(errorEvent); } function writeLog(level, label, args) { if (label.indexOf(":minor") !== -1) { return; } const message = util.format(...args); const data = { label, message, objects: args }; const entry = gcpLog.entry(data); gcpLog[level](entry); if (level === "error") { reportError(label, label + " " + message); } } module.exports = { reportError, writeLog }; ================================================ FILE: shared/stackdriver/package.json ================================================ { "name": "@oo/shared_stackdriver", "version": "0.0.0", "description": "GCP Stackdriver bindings for Node.js", "main": "index.js", "scripts": {}, "author": "Shane F. Carr", "license": "AGPL-3.0", "dependencies": { "@google-cloud/error-reporting": "^2", "@google-cloud/logging": "^9" } } ================================================ FILE: shared/stdio-messenger.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const OnlineOffline = require("./online-offline"); const Queue = require("./queue"); const JSONStreamSafe = require("./json-stream-safe"); class StdioMessenger extends OnlineOffline { constructor() { super(); // Message queue ("buffer") for outgoing messages this._messageQueue = new Queue(); this._messageQueue.on("enqueue", this._flush.bind(this)); this._readStream = null; this._writeStream = null; } setReadStream(stream) { if (this._readStream) throw new Error("Can set only one read stream"); // TODO: Will the JSONStreamSafe ever be garbage collected? The underlying stream will be closed when the session dies; is that sufficient? Is it dangerous that the callback references "this"? this._readStream = new JSONStreamSafe(stream); this._readStream.on("data", this._handleData.bind(this)); this._readStream.on("error", (err) => { this.emit("error", err); }); } setWriteStream(stream) { if (this._writeStream) throw new Error("Can set only one write stream"); this._writeStream = stream; this._flush(); } sendMessage(name, content) { const message = JSON.stringify([name, content]); this._messageQueue.enqueue(message); } _flush() { if (!this._writeStream) return this._log.warn("Message stream unavailable"); if (!this._writeStream.writable) return this._log.warn("Message stream not writable"); while (!this._messageQueue.isEmpty()) { this._writeStream.write(this._messageQueue.dequeue()); } } _handleData(data) { if (!Array.isArray(data) || data.length !== 2 || typeof data[0] !== "string") { return this.emit("error", new Error(`Malformed message: '${data}'`)); } this.emit("message", data[0], data[1]); this.emit(`msg:${data[0]}`, data[1]); } } module.exports = StdioMessenger; ================================================ FILE: shared/time-limit.js ================================================ /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // From https://github.com/caolan/async/issues/1007 // (my contribution) function timeLimit(milliseconds, defaults, callback) { var timer, normalCallbackRef; var normalCallback = function() { callback.apply(null, arguments); clearTimeout(timer); }; var timeoutCallback = function() { callback.apply(null, defaults); normalCallbackRef = function(){}; // noop }; timer = setTimeout(timeoutCallback, milliseconds); normalCallbackRef = normalCallback; return function() { normalCallbackRef.apply(null, arguments); }; } module.exports = timeLimit; ================================================ FILE: test/package.json ================================================ { "name": "@oo/test", "version": "0.0.0", "description": "Unit tests for Octave Online (incomplete)", "scripts": { "test": "ava" }, "ava": { "files": [ "*.js" ] }, "author": "Shane F. Carr", "license": "AGPL-3.0", "devDependencies": { "@oo/shared": "file:../shared", "ava": "^3.9.0" } } ================================================ FILE: test/small-unit.js ================================================ /* * Copyright © 2019, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // Small unit tests that are fast and don't require external services "use strict"; const stream = require("stream"); const test = require("ava"); const shared = require("@oo/shared"); test("json byte stream", (t) => { { const pt = new stream.PassThrough(); const jss = new shared.JSONStreamSafe(pt); // Objects that are expected out of the stream: const expectedObjects = [ ["a", "b"], ["c"], "�", { key: "�Ȗ��" }, [] ]; let i = 0; // let closed = false; jss.on("data", (data) => { t.deepEqual(data, expectedObjects[i++], `Index ${i}`); }); jss.on("end", () => { // closed = true; }); // Write data to the stream: pt.write("[\"a\",\"b\"][\"c\"]"); pt.write(Buffer.from([34, 128, 34])); pt.write("{\"key\":\""); pt.write(Buffer.from([ 237, 137, // 3-byte character missing third byte 200, 150, // valid 2-byte char 150, // dangling continuation char 250 // lead char with nothing following ])); pt.write("\"}"); pt.write("[]"); pt.end(); // All objects should have been read: t.is(i, expectedObjects.length); // TODO: It seems the end event does not bubble through. // t.true(closed); } }); ================================================ FILE: utils-admin/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: utils-admin/README.md ================================================ Octave Online Server: Admin Panel ================================= This directory contains a standalone administration panel service for Octave Online Server. It interacts with the MongoDB database to help create and set up user accounts. ================================================ FILE: utils-admin/app.js ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const createError = require("http-errors"); const express = require("express"); const path = require("path"); const cookieParser = require("cookie-parser"); const logger = require("morgan"); const basicAuth = require("express-basic-auth"); const config = require("@oo/shared").config; const indexRouter = require("./routes/index"); const usersRouter = require("./routes/users"); const app = express(); // view engine setup app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(logger("dev")); app.use(basicAuth({ users: config.auth.utils_admin.users, challenge: true })); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); app.use("/", indexRouter); app.use("/users", usersRouter); // catch 404 and forward to error handler app.use(function(req, res, next) { next(createError(404)); }); // error handler app.use(function(err, req, res) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page res.status(err.status || 500); res.render("error"); }); module.exports = app; ================================================ FILE: utils-admin/bin/repo-cleanup.js ================================================ #!/usr/bin/env node /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ // This tool deletes repos from inactive users. /* eslint-disable no-console */ const config = require("@oo/shared").config; const db = require("../src/db"); const debug = require("debug")("oo:repo-cleanup"); const gcp = require("../../shared/gcp/index.js"); const repo = require("../src/repo"); const command = process.argv[2]; if (command !== "run" && command !== "dryrun") { console.log("Usage: DEBUG=oo:* bin/repo-cleanup.js [dry]run [30 [200]]"); return; } (async function() { // Database connection. const mongoUrl = `mongodb://${config.mongo.hostname}:${config.mongo.port}`; const mongoDb = config.mongo.db; await db.connect(mongoUrl, mongoDb); debug("Connected to MongoDB:", mongoUrl, mongoDb); let numDays = process.argv[3] ? parseInt(process.argv[3]) : 30; debug("Number of days to clean:", numDays); let skipDays = process.argv[4] ? parseInt(process.argv[4]) : 200; debug("End at this many days in the past:", skipDays); let startTime = new Date(); startTime.setDate(startTime.getDate() - skipDays - numDays); debug("Start time:", startTime); let endTime = new Date(); endTime.setDate(endTime.getDate() - skipDays); debug("End time:", endTime); const query = { "last_activity": { "$gt": startTime, "$lt": endTime, }, "patreon.currently_entitled_amount_cents": { "$in": [null, 0], }, }; for await (let user of db.findAll("users", query, { parametrized: 1, last_activity: 1 })) { debug("Processing:", JSON.stringify(user)); if (command === "run") { await gcp.uploadRepoArchive(debug, "repos", user.parametrized); await repo.deleteRepo(user); } } db.close(); // async function })().catch((err) => { throw err; }); ================================================ FILE: utils-admin/bin/server.js ================================================ #!/usr/bin/env node /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ /** * Module dependencies. */ const app = require("../app"); const db = require("../src/db"); const debug = require("debug")("oo:server"); const http = require("http"); const config = require("@oo/shared").config; (async function() { // Database connection. const mongoUrl = `mongodb://${config.mongo.hostname}:${config.mongo.port}`; const mongoDb = config.mongo.db; await db.connect(mongoUrl, mongoDb); debug("Connected to MongoDB:", mongoUrl, mongoDb); // Get port from environment and store in Express. const port = normalizePort(process.env.PORT || "3000"); app.set("port", port); // Create HTTP server. const server = http.createServer(app); // Listen on provided port, on all network interfaces. server.listen(port); server.on("error", onError); server.on("listening", onListening); // Normalize a port into a number, string, or false. function normalizePort(val) { const port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } // Event listener for HTTP server "error" event. function onError(error) { if (error.syscall !== "listen") { throw error; } const bind = typeof port === "string" ? "Pipe " + port : "Port " + port; // handle specific listen errors with friendly messages switch (error.code) { case "EACCES": debug(bind + " requires elevated privileges"); process.exit(1); break; case "EADDRINUSE": debug(bind + " is already in use"); process.exit(1); break; default: throw error; } } // Event listener for HTTP server "listening" event. function onListening() { const addr = server.address(); const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; debug("Listening on " + bind); } // async function })().catch((err) => { throw err; }); ================================================ FILE: utils-admin/package.json ================================================ { "name": "@oo/utilsadmin", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/server.js" }, "author": "Octave Online LLC", "license": "AGPL-3.0", "dependencies": { "@awaitjs/express": "^0.4.0", "@oo/shared": "file:../shared", "@oo/shared_gcp": "file:../shared/gcp", "bcryptjs": "^2.4.3", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "~2.6.1", "express": "~4.16.1", "express-basic-auth": "^1.2.0", "got": "^11.8.2", "http-errors": "~1.6.3", "mongodb": "^3.6.10", "morgan": "~1.9.1" } } ================================================ FILE: utils-admin/public/stylesheets/style.css ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ body { max-width: 640px; background-color: #AD928E; margin: 0 auto; padding: 0; /*font:12px/16px Verdana, sans-serif;*/ } div#main { background-color: #FFF; margin: 0; padding: 10px; } ================================================ FILE: utils-admin/routes/index.js ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const express = require("express"); const { addAsync } = require("@awaitjs/express"); const db = require("../src/db"); const router = addAsync(express.Router()); router.get("/", function(req, res) { res.render("index", { title: "OO Admin Portal", top: true }); }); router.getAsync("/find/", async function(req, res) { const queryString = req.query.mq || "{}"; let queryObject; try { queryObject = JSON.parse(queryString); } catch(e) { res.status(400).type("txt").send(`JSON parse error: ${e.message}\n\n${queryString}`); return; } const docs = await db.find(req.query.collection, queryObject); res.render("find", { title: "OO Find", docs, query: req.query }); }); router.postAsync("/create.do", async function(req, res) { const newDoc = JSON.parse(req.body.document); const result = await db.createDocument("users", newDoc); res.redirect(`users/${result.ops[0]._id}`); }); module.exports = router; ================================================ FILE: utils-admin/routes/users.js ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const crypto = require("crypto"); const util = require("util"); const bcryptjs = require("bcryptjs"); const express = require("express"); const { addAsync } = require("@awaitjs/express"); const { config, logger } = require("@oo/shared"); const db = require("../src/db"); const gcp = require("@oo/shared_gcp"); const repo = require("../src/repo"); const router = addAsync(express.Router()); function randomString(length) { const base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); const bytes = crypto.randomBytes(length); let str = ""; for (var i = 0; i < length; i++) { str += base[bytes[i] % base.length]; } return str; } router.getAsync("/", async function(req, res) { const queryString = req.query.mq || "{}"; let queryObject; try { queryObject = JSON.parse(queryString); } catch(e) { res.status(400).type("txt").send(`JSON parse error: ${e.message}\n\n${queryString}`); return; } const users = await db.find("users", queryObject); res.render("user-list", { title: "OO User Search", users, queryString }); }); router.getAsync("/:userId", async function(req, res) { const userId = req.params.userId || ""; const user = await db.findById("users", userId); const buckets = await db.find("buckets", { "user_id": user._id, }); res.render("user", { title: `OO: ${user.email || `Deleted User ${user.deleted_email}`}`, user, buckets, randomString: randomString(12), config }); }); router.postAsync("/:userId/add-code.do", async function(req, res) { const userId = req.params.userId || ""; await db.updateById("users", userId, { $addToSet: { instructor: req.body.courseCode } }); res.redirect("."); }); router.postAsync("/:userId/set-password.do", async function(req, res) { const userId = req.params.userId || ""; const rawPassword = req.body.string; const salt = await util.promisify(bcryptjs.genSalt)(config.auth.password.salt_rounds); const hash = await util.promisify(bcryptjs.hash)(rawPassword, salt); await db.updateById("users", userId, { $set: { password_hash: hash } }); res.redirect("."); }); router.postAsync("/:userId/overwrite.do", async function(req, res) { const userId = req.params.userId || ""; let newDoc; try { newDoc = JSON.parse(req.body.document); } catch(e) { res.status(400).type("txt").send(`JSON parse error: ${e.message}\n\n${req.body.document}`); return; } await db.replaceById("users", userId, newDoc); res.redirect("."); }); router.postAsync("/:userId/delete-data.do", async function(req, res) { const userId = req.params.userId || ""; const user = await db.findById("users", userId); if (req.body.deleteRepo) { await repo.deleteRepo(user); } if (req.body.deleteMongo) { const newDoc = { deleted_email: user.email, parametrized: user.parametrized, }; await db.replaceById("users", userId, newDoc); } res.redirect("."); }); router.postAsync("/:userId/restore-from-storage.do", async function(req, res) { const userId = req.params.userId || ""; const user = await db.findById("users", userId); const gsUri = req.body.gsUri; const matches = /^gs:\/\/([^\/]+)\/(.+)$/.exec(gsUri); if (!matches) { res.status(400).type("txt").send(`gsUri is not valid: ${gsUri}`); return; } const log = logger(`restore:${userId}`); log.info("Beginning restoration process:", gsUri); try { await gcp.restoreRepoFromCloudStorage(log.log, "repos", user.parametrized, matches[1], matches[2]); } catch(e) { res.status(500).type("txt").send(`Error while restoring files:\n\n${e}`); log.error(e); return; } res.redirect("."); }); module.exports = router; ================================================ FILE: utils-admin/src/db.js ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const mongodb = require("mongodb"); let mongoClient = null; let db = null; async function connect(url, dbName) { mongoClient = new mongodb.MongoClient(url); await mongoClient.connect(); db = mongoClient.db(dbName); } async function close() { mongoClient.close(); } async function find(collectionName, query) { const collection = db.collection(collectionName); return await collection.find(query).limit(10).toArray(); } async function findById(collectionName, id) { const collection = db.collection(collectionName); const result = await collection.findOne({ _id: new mongodb.ObjectId(id) }); if (!result) { throw new Error("Could not find user with id: " + id); } return result; } function findAll(collectionName, query, projection) { const collection = db.collection(collectionName); const cursor = collection.find(query).project(projection).batchSize(50); return cursor; } async function updateById(collectionName, id, update) { const collection = db.collection(collectionName); return await collection.updateOne({ _id: new mongodb.ObjectId(id) }, update); } async function replaceById(collectionName, id, newDoc) { const collection = db.collection(collectionName); return await collection.replaceOne({ _id: new mongodb.ObjectId(id) }, newDoc); } async function createDocument(collectionName, newDoc) { const collection = db.collection(collectionName); return await collection.insertOne(newDoc); } module.exports = { connect, close, find, findById, findAll, updateById, replaceById, createDocument }; ================================================ FILE: utils-admin/src/repo.js ================================================ /* * Copyright © 2020, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ const got = require("got"); const config = require("@oo/shared").config; async function deleteRepo(user) { return await got(`http://${config.git.hostname}:${config.git.createRepoPort}`, { searchParams: { type: "repos", name: user.parametrized, action: "delete", }, retry: 0, }); } module.exports = { deleteRepo, }; ================================================ FILE: utils-admin/views/error.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %>

<%= message %>

<%= error.status %>

<%= error.stack %>
================================================ FILE: utils-admin/views/find.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/header"); -%> <% if (docs.length === 0) { %>

No documents found!

<% } else { %>

Query Results

<% for (let doc of docs) { %>
<%= JSON.stringify(doc, null, 2) %>
<% } %>
<% } %>

<%- include("partials/footer"); -%> ================================================ FILE: utils-admin/views/index.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/header"); -%>

Search for User



<%- include("partials/footer"); -%> ================================================ FILE: utils-admin/views/partials/footer.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %>
================================================ FILE: utils-admin/views/partials/header.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%= title %>
<% if (!locals.top) { %> « Home <% } %>

<%= title %>

================================================ FILE: utils-admin/views/user-list.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/header"); -%> <% if (users.length === 0) { %>

No users found!

<% } else { %> <% } %>

<%- include("partials/footer"); -%> ================================================ FILE: utils-admin/views/user.ejs ================================================ <%# Copyright © 2020, Octave Online LLC This file is part of Octave Online Server. Octave Online Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Octave Online Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Octave Online Server. If not, see . %> <%- include("partials/header"); -%>

Raw MongoDB Document

<%= JSON.stringify(user, null, 2) %>

Add Instructor Course Code

Set Password

Buckets

<% if (buckets.length) { %>
    <% for (let bucket of buckets) { %>
  • <%= bucket.bucket_id %> (<%= bucket._id.getTimestamp().toISOString() %>)
  • <% } %>
<% } else { %>

No buckets found

<% } %>

Repo Operations

Open Repo Viewer

Delete User Data



Note: Bucket deletion not supported yet

Overwrite Whole Document

<% userFiltered = Object.assign({}, user); delete userFiltered._id; %>

<%- include("partials/footer"); -%> ================================================ FILE: utils-auth/.eslintrc.yml ================================================ rules: # Allow console.log no-console: - off ================================================ FILE: utils-auth/.npmrc ================================================ # Please keep this in sync with all other .npmrc files # SEE: https://github.com/npm/feedback/discussions/864 install-links=false ================================================ FILE: utils-auth/README.md ================================================ Octave Online Server: Authentication Service ============================================ This directory contains a standalone service for authenticating user accounts stored in MongoDB. It is a very basic HTTP server that uses HTTP Basic Auth to authenticate against the MongoDB user database. This service was designed for plugging into Nginx to allow for authenticated https access to Git repositories, a feature on octave-online.net. **Note: Most users do not need to worry about the code in this directory.** ================================================ FILE: utils-auth/app.js ================================================ #!/usr/bin/env node /* * Copyright © 2018, Octave Online LLC * * This file is part of Octave Online Server. * * Octave Online Server is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Octave Online Server is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Octave Online Server. If not, see * . */ "use strict"; const http = require("http"); const mongoose = require("mongoose"); const bcrypt = require("bcrypt"); const config = require("@oo/shared").config; const path = require("path"); const basicAuth = require("basic-auth"); const fs = require("fs"); //const PORT = 5123; const SOCKET_PATH = "/var/run/oosocks/auth.sock"; try { fs.unlinkSync(SOCKET_PATH); } catch(err) { // ignored; socket file might not exist } console.log("Listening on UNIX socket " + SOCKET_PATH); const mongoUrl = `mongodb://${config.mongo.hostname}:${config.mongo.port}/${config.mongo.db}`; mongoose.connect(mongoUrl, { useNewUrlParser: true }).then(() => { console.log("Connected to MongoDB at", mongoUrl); }).catch((err) => { console.error("FAILED TO CONNECT TO MONGODB", mongoUrl, err); process.exit(1); }); const User = mongoose.model("User", { email: String, parametrized: String, password_hash: String }); var benchmarkMongoAvg = 0.0; var benchmarkMongoCnt = 0; var benchmarkBcryptAvg = 0.0; var benchmarkBcryptCnt = 0; const server = http.createServer((req, res) => { const originalUri = path.normalize(req.headers["x-original-uri"] || ""); const match1 = /^\/(\w+)\.git\/.*$/.exec(originalUri); const match2 = /^\/(src|themes|vendor)\/.*$/.exec(originalUri); if (match2 !== null) { res.writeHead(204); return res.end(); } if (match1 === null) { res.writeHead(403); return res.end(); } const parametrized = match1[1]; if (req.headers["authorization"]) { // Check username/password const creds = basicAuth.parse(req.headers["authorization"]); if (!creds) { res.writeHead(400); return res.end("Invalid credentials"); } const adminPass = config.auth.utils_admin.users[creds.name]; if (adminPass && creds.pass === adminPass) { res.writeHead(204); return res.end(); } const mongoStart = new Date().valueOf(); User.findOne({ email: creds.name }, (err, user) => { const mongoEnd = new Date().valueOf(); benchmarkMongoAvg = benchmarkMongoAvg + (mongoEnd-mongoStart-benchmarkMongoAvg)/(benchmarkMongoCnt+1); benchmarkMongoCnt++; if (err || !user) { console.error(err || "User not found"); return prompt(res, "Make sure you entered the correct email/password"); } if (!user.password_hash) { return prompt(res, "Make sure you have set a password"); } if (user.parametrized !== parametrized) { return prompt(res, "Make sure your repository path is correct"); } const bcryptStart = new Date().valueOf(); bcrypt.compare(creds.pass, user.password_hash, (err, valid) => { const bcryptEnd = new Date().valueOf(); benchmarkBcryptAvg = benchmarkBcryptAvg + (bcryptEnd-bcryptStart-benchmarkBcryptAvg)/(benchmarkBcryptCnt+1); benchmarkBcryptCnt++; if (err) { console.error(err); res.writeHead(400); return res.end("Problem validating password"); } if (!valid) { return prompt(res, "Make sure you entered the correct email/password"); } res.writeHead(204); return res.end(); }); }); } else { // Prompt for username/password prompt(res, "Please enter your email and password"); } }); server.listen(SOCKET_PATH); function prompt(res, message) { res.writeHead(401, { "WWW-Authenticate": "Basic realm=\""+message+"\"" }); return res.end(); } setInterval(() => { console.log("Mongo: " + benchmarkMongoAvg + " (" + benchmarkMongoCnt + " reqs); " + "Bcrypt: "+ benchmarkBcryptAvg+ " (" + benchmarkBcryptCnt+ " reqs)"); }, 86400000); ================================================ FILE: utils-auth/configs/custom_4xx.html ================================================ Octave Online

When prompted for a username and password, enter your email address and your Octave Online password. Create a password for your Octave Online account by clicking "Change Password" in the main menu on the home page.

================================================ FILE: utils-auth/configs/gitlist.ini ================================================ ; Copyright © 2019, Octave Online LLC ; ; This file is part of Octave Online Server. ; ; Octave Online Server is free software: you can redistribute it and/or modify ; it under the terms of the GNU Affero General Public License as published by ; the Free Software Foundation, either version 3 of the License, or (at your ; option) any later version. ; ; Octave Online Server is distributed in the hope that it will be useful, but ; WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public ; License for more details. ; ; You should have received a copy of the GNU Affero General Public License ; along with Octave Online Server. If not, see ; . ; This is the Octave Online config for the GitList instance [git] client = '/usr/bin/git' ; Your git executable path default_branch = 'master' ; Default branch when HEAD is detached repositories[] = '/srv/oo/git/repos' [app] debug = false cache = true theme = 'default' title = 'Octave Online File History' [clone_button] ; ssh remote show_ssh_remote = false ; display remote URL for SSH ; http remote show_http_remote = true ; display remote URL for HTTP ;http_host = '' ; host to use for cloning via HTTP (default: none => uses gitlist web host) use_https = "auto" ; generate URL with https:// ;http_url_subdir = 'git/' ; if cloning via HTTP is triggered using virtual dir (e.g. https://example.com/git/repo.git) ; has to end with trailing slash ;http_user = '' ; user to use for cloning via HTTP (default: none) http_user_dynamic = true ; when enabled, http_user is set to $_SERVER['PHP_AUTH_USER'] ; If you need to specify custom filetypes for certain extensions, do this here [filetypes] ; extension = type ; dist = xml ; If you need to set file types as binary or not, do this here [binary_filetypes] ; extension = true ; svh = false ; map = true ; set the timezone [date] ; timezone = UTC ; format = 'd/m/Y H:i:s' ; custom avatar service [avatar] ; url = '//gravatar.com/avatar/' ; query[] = 'd=identicon' ================================================ FILE: utils-auth/configs/oo-utils-auth.service ================================================ # Copyright © 2018, Octave Online LLC # # This file is part of Octave Online Server. # # Octave Online Server is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # Octave Online Server is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with Octave Online Server. If not, see # . ############################################################### # NOTE: This systemd service file is here for reference only; # # it is not currently being used in Octave Online Server. # ############################################################### [Unit] Description="Authenticates users against the Mongo database" [Service] ExecStart=/srv/oo/projects/utils-auth/app.js StandardOutput=syslog StandardError=syslog SyslogIdentifier=oo-utils-auth User=nginx Group=nginx RuntimeDirectory=oosocks Environment=NODE_ENV=production Restart=always [Install] WantedBy=multi-user.target ================================================ FILE: utils-auth/package.json ================================================ { "name": "@oo/utilsauth", "version": "0.0.0", "description": "Authenticates users accessing git repos on the back server", "main": "app.js", "scripts": {}, "author": "Octave Online LLC", "license": "AGPL-3.0", "private": true, "dependencies": { "@oo/shared": "file:../shared", "basic-auth": "^1.1.0", "bcrypt": "^5.0.1", "mongoose": "^5.10.19" } }