├── .gitignore ├── .gitlab-ci.yml ├── AUTHORS ├── CHANGELOG ├── LICENSE ├── README.md ├── api.md ├── backend ├── .cargo │ └── config.toml ├── .dockerignore ├── .env.sample ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── README.md ├── build.sh ├── diesel.toml ├── jielabs-backend.service ├── migrations │ ├── .gitkeep │ ├── 2020-02-12-120630_create_users │ │ ├── down.sql │ │ └── up.sql │ ├── 2020-02-13-144220_create_jobs │ │ ├── down.sql │ │ └── up.sql │ ├── 2020-02-16-121048_create_configs │ │ ├── down.sql │ │ └── up.sql │ ├── 2020-02-23-082509_add_time_to_jobs │ │ ├── down.sql │ │ └── up.sql │ ├── 2020-03-07-000036_add_last_login_to_users │ │ ├── down.sql │ │ └── up.sql │ └── 2021-03-16-124339_make_password_nullable │ │ ├── down.sql │ │ └── up.sql └── src │ ├── bin │ ├── backend.rs │ ├── create_user.rs │ ├── create_users.rs │ ├── fix_jobs.rs │ ├── generate_hash.rs │ ├── mock_board.rs │ ├── mock_user.rs │ └── s3_gc.rs │ ├── board.rs │ ├── board_manager.rs │ ├── common.rs │ ├── env.rs │ ├── file.rs │ ├── lib.rs │ ├── metric.rs │ ├── models.rs │ ├── schema.rs │ ├── session.rs │ ├── task.rs │ ├── task_manager.rs │ ├── user.rs │ ├── ws_board.rs │ └── ws_user.rs ├── examples ├── 4m-clock-digital-life.json ├── const-output.json ├── digital_life.vhdl ├── manual-clock-digital-life.json ├── multi-pin-net-manual-clock.json ├── running-light.json └── running_light.vhdl ├── frontend ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── assets │ └── logo.studio ├── build.sh ├── config-overrides.js ├── mime.types ├── nginx.conf ├── package.json ├── public │ ├── index.html │ ├── logo │ │ ├── logo-flat.png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo@2x.png │ │ ├── logo@3x.png │ │ ├── logo@4x.png │ │ └── logo@8x.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── ErrorBoundary.js │ ├── HelpLayer.js │ ├── Sandbox.js │ ├── assets │ │ ├── tutorial.v │ │ └── tutorial.vhdl │ ├── blocks │ │ ├── Clock.js │ │ ├── Digit4.js │ │ ├── Digit7.js │ │ ├── FPGA.js │ │ ├── Switch4.js │ │ └── index.js │ ├── comps │ │ ├── Dialog.js │ │ ├── Digit.js │ │ ├── Highlighter.js │ │ ├── Icon.js │ │ ├── Input.js │ │ └── Tooltip.js │ ├── config.example.js │ ├── fonts │ │ ├── MaterialIcons │ │ │ └── MaterialIcons.woff2 │ │ ├── Roboto │ │ │ ├── LICENSE.txt │ │ │ ├── Roboto-Bold.woff2 │ │ │ ├── Roboto-Regular.woff2 │ │ │ └── Roboto-Thin.woff2 │ │ └── RobotoMono │ │ │ ├── LICENSE.txt │ │ │ ├── RobotoMono-Bold.woff2 │ │ │ ├── RobotoMono-Regular.woff2 │ │ │ └── RobotoMono-Thin.woff2 │ ├── index.js │ ├── index.scss │ ├── lang.js │ ├── lib │ │ ├── .gitignore │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ ├── verilog.rs │ │ │ └── vhdl.rs │ ├── loaders │ │ └── Monaco.js │ ├── polyfills.js │ ├── prelude.scss │ ├── routes │ │ ├── Login.js │ │ └── Workspace.js │ ├── serviceWorker.js │ ├── store │ │ ├── actions.js │ │ ├── index.js │ │ └── reducers.js │ ├── styles │ │ ├── consts.scss │ │ ├── editor.scss │ │ ├── error.scss │ │ ├── font.scss │ │ ├── help.scss │ │ ├── highlighter.scss │ │ ├── scrollbar.scss │ │ ├── shutter.scss │ │ ├── tooltip.scss │ │ └── transition.scss │ └── util.js └── yarn.lock ├── manage ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── components │ │ └── Main.vue │ ├── config.sample.js │ ├── main.js │ ├── plugins │ │ └── vuetify.js │ └── util.js ├── vue.config.js └── yarn.lock ├── protocol.md └── uninstaller └── service-worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | deploy.sh 2 | config.yml 3 | .*.sw[a-p] 4 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:20.10.8 2 | 3 | variables: 4 | DOCKER_HOST: tcp://docker:2376 5 | # 6 | # The 'docker' hostname is the alias of the service container as described at 7 | # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services. 8 | # If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier, 9 | # the variable must be set to tcp://localhost:2376 because of how the 10 | # Kubernetes executor connects services to the job container 11 | # DOCKER_HOST: tcp://localhost:2376 12 | # 13 | # Specify to Docker where to create the certificates, Docker will 14 | # create them automatically on boot, and will create 15 | # `/certs/client` that will be shared between the service and job 16 | # container, thanks to volume mount from config.toml 17 | DOCKER_TLS_CERTDIR: "/certs" 18 | # These are usually specified by the entrypoint, however the 19 | # Kubernetes executor doesn't run entrypoints 20 | # https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4125 21 | DOCKER_TLS_VERIFY: 1 22 | DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client" 23 | DOCKER_DAEMON_OPTIONS: "--insecure-registry=${REGISTRY}" 24 | 25 | services: 26 | - name: docker:20.10.8-dind 27 | entrypoint: ["sh", "-c", "dockerd-entrypoint.sh $DOCKER_DAEMON_OPTIONS"] 28 | 29 | stages: 30 | - build 31 | - deploy 32 | 33 | build/frontend: 34 | stage: build 35 | script: 36 | - until docker info; do sleep 1; done 37 | - echo "$REGISTRY_PASS" | docker login $REGISTRY --username $REGISTRY_USER --password-stdin 38 | - cd frontend && ./build.sh 39 | 40 | build/backend: 41 | stage: build 42 | script: 43 | - until docker info; do sleep 1; done 44 | - echo "$REGISTRY_PASS" | docker login $REGISTRY --username $REGISTRY_USER --password-stdin 45 | - cd backend && ./build.sh 46 | 47 | deploy: 48 | stage: deploy 49 | image: bitnami/kubectl:1.20 50 | environment: 51 | name: production 52 | only: 53 | - master 54 | script: 55 | - kubectl -n jielabs rollout restart deployment/jielabs-backend 56 | - kubectl -n jielabs rollout restart deployment/jielabs-frontend 57 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Chen Jiajie 2 | Gao Yichuan 3 | Liu Xiaoyi 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | JieLabs 版本可以从设定 -> About 中查看。JieLabs 每五分钟自动检查一次更新(或者在 2020-3-16 之前,手动刷新后检查更新),如有更新会在右下角通知区显示新版本通知。 4 | 5 | 截止至版本 heads/production-0-g0a342da-dirty 自从内测开始 (2020-3-11日) 以来,JieLabs 有如下更新: 6 | 7 | ## 修正 8 | 9 | - FPGA 引脚修改为和真实硬件一致 10 | - 断开连线时可能不更新连线两端的信号 11 | - 有时手动时钟不起作用 12 | - 小屏幕下教程文字溢出屏幕 13 | - 译码七段数码管在输入大于 9 时现在不显示任何段,而不是 A-F,和硬件保持一致 14 | - Edge 浏览器无法加载 15 | - 加载存档的时候高频时钟可能不生效 16 | - Verilog 模式下部分语法导致页面卡死 17 | - 部分情况下信号的初始状态不会得到反馈 18 | - 信号刷新频率过高导致系统过载的问题 19 | - Verilog 在构建详情中不被高亮 20 | - 标签页放置一段时间后显示 Build detail 会导致异常 21 | - 加载存档时可能发生异常 22 | 23 | ## 新增功能 24 | 25 | - 管理端大幅度更新: 26 | - 管理端添加定位实验板功能 27 | - 管理端统计在线用户数 28 | - 添加 CI 测试 29 | - 自动保存沙箱连线,刷新后重载 30 | - 添加报错页面,以及错误信息采集 31 | - 加载存档、刷新前后保持选项中的语言设定 32 | - 在手动刷新以外,定时检查是否有更新 33 | - VHDL 语言支持更新到了 VHDL-2008 34 | - Sentry error logging 35 | - 可以一键删除所有连线 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JieLabs Web 2 | 3 | **JieLabs** 是清华大学计算机系的《数字逻辑实验》课程在 2020 年春季学期授课使用的在线实验平台。本仓库包含 JieLabs 的 Web 后端和前端部分。 4 | 5 | 后端和前端的构建方法请具体查看 [backend](https://github.com/thu-cs-lab/JieLabs-Web/tree/master/backend) 和 [frontend](https://github.com/thu-cs-lab/JieLabs-Web/tree/master/frontend) 目录中的具体说明。 6 | 7 | ## 作者 8 | JieLabs 由陈嘉杰、高一川、刘晓义进行开发和维护。 9 | 10 | ## 开放源代码协议 11 | 12 | 本项目源代码使用 AGPL 3.0 协议进行分发,协议的具体条款可以参见本项目根目录中的 LICENSE 文件。请在使用本项目源码及相关资源时注意使用方法是否与协议相冲突。 13 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # 前后端通信的 API Endpoint 2 | 3 | ## 登录状态 4 | 5 | ### 登录 6 | 7 | POST /api/session 8 | 9 | ### 登出 10 | 11 | DELETE /api/session 12 | 13 | ### 获取登录用户信息 14 | 15 | GET /api/session 16 | 17 | ## 用户管理 18 | 19 | ### 列出所有用户 20 | 21 | GET /api/user/list?offset=0&limit=5 22 | 23 | 仅 admin 可用 24 | 25 | ### 获取用户数量 26 | 27 | GET /api/user/count 28 | 29 | 仅 admin 可用 30 | 31 | ### 更新用户信息 32 | 33 | POST /api/user/manage/{user_name} 34 | 35 | admin 可用字段:real_name class student_id role password 36 | 37 | 所有人可更新自己的,可用字段:real_name class student_id role password 38 | 39 | ### 创建用户 40 | 41 | PUT /api/user/manage/{user_name} 42 | 43 | 可用字段:real_name class student_id role password 44 | 45 | 仅 admin 可用 46 | 47 | ### 获取用户信息 48 | 49 | GET /api/user/manage/{user_name} 50 | 51 | 仅 admin 可用 52 | 53 | ### 删除用户 54 | 55 | DELETE /api/user/manage/{user_name} 56 | 57 | 仅 admin 可用 58 | 59 | ## 板子管理 60 | 61 | ### 列出所有板子 62 | 63 | GET /api/board/list 64 | 65 | 仅 admin 可用 66 | 67 | ### 设置固件信息 68 | 69 | POST /api/board/version 70 | 71 | 字段:version,版本信息;url,下载地址;hash,文件的 sha1sum 72 | 73 | 仅 admin 可用 74 | 75 | ### 获取固件信息 76 | 77 | GET /api/board/version 78 | 79 | 内容有三行:版本信息;下载地址;文件哈希 80 | 81 | ### 设置板子配置 82 | 83 | POST /api/board/config 84 | 85 | 字段:board:板子 ip,ident:bool 86 | 87 | 仅 admin 可用 88 | 89 | ## 文件管理 90 | 91 | ### 上传文件 92 | 93 | GET /api/file/upload 94 | 95 | 获得一个文件 ID 和链接,对这个链接 PUT 文件内容即可上传。 96 | 97 | ## 任务管理 98 | 99 | ### 提交构建任务 100 | 101 | POST /api/task/build 102 | 103 | 字段:source,通过 /api/file/upload 获取的附件 ID 104 | 105 | 获得 job_id,可以用这个 ID 获取构建信息 106 | 107 | ### 获取构建信息 108 | 109 | GET /api/task/get/{job_id} 110 | 111 | 仅构建的创建用户和admin可访问 112 | 113 | ### 提交构建结果 114 | 115 | POST /api/task/finish 116 | 117 | 字段:task_id,表示 task 的ID 118 | 119 | ### 获取任务信息 120 | 121 | GET /api/task/list?offset=0&limit=5 122 | 123 | 仅 admin 可用 124 | 125 | ### 获取任务信息 126 | 127 | GET /api/task?offset=0&limit=5 128 | 129 | 获取用户自己提交的 task -------------------------------------------------------------------------------- /backend/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [source.crates-io] 2 | replace-with = 'tuna' 3 | 4 | [source.tuna] 5 | registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" 6 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | /target 3 | -------------------------------------------------------------------------------- /backend/.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://localhost/jielabs 2 | COOKIE_SECRET=REDACTED 3 | PASSWORD_SECRET=REDACTED 4 | BOARD_PASS=REDACTED 5 | METRIC_AUTH=REDACTED 6 | #ALLOW_ANONYMOUS_WS_USER=yes 7 | S3_ENDPOINT=http://127.0.0.1:9000 8 | S3_BUCKET=jielabs-data 9 | S3_KEY=minioadmin 10 | S3_SECRET=minioadmin 11 | S3_REGION=something 12 | REDIS_URL=redis://127.0.0.1/ 13 | REDIS_WAITING_QUEUE=jielabs-waiting 14 | REDIS_WORKING_QUEUE=jielabs-working 15 | #SENTRY_URL=https://REDACTED@sentry.io/REDACTED 16 | PORTAL_CLIENT_SECRET=REDACTED -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .env 4 | *.db 5 | *.rbf 6 | *.csv 7 | .*.sw[a-p] 8 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.1.0" 4 | authors = ["Jiajie Chen "] 5 | edition = "2018" 6 | default-run = "backend" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | actix = "0.10.0" 12 | actix-cors = "0.5.4" 13 | actix-http = "2.2.0" 14 | actix-web = "3.3.2" 15 | actix-web-actors = "3.0.0" 16 | actix-web-httpauth = "0.5.0" 17 | actix-rt = "1.1.1" 18 | bimap = "0.5.3" 19 | bytes = "0.5" 20 | chrono = { version = "0.4.19", features = ["serde"] } 21 | csv = "1.1.5" 22 | diesel = { version = "1.4.5", features = ["postgres", "r2d2", "chrono"] } 23 | diesel_migrations = "1.4.0" 24 | dotenv = "0.9.0" 25 | env_logger = "0.8.2" 26 | failure = "0.1.8" 27 | futures = "0.3.13" 28 | log = "0.4.11" 29 | lazy_static = "1.4" 30 | paw = "1.0.0" 31 | rand = "0.8.0" 32 | redis = "0.18.0" 33 | ring = "0.16.19" 34 | rusoto_core = "0.45.0" 35 | rusoto_s3 = "0.45.0" 36 | serde = "1.0.118" 37 | serde_json = "1.0" 38 | serde_derive = "1.0" 39 | sentry = "0.21.0" 40 | structopt = { version = "0.3.21", features = ["paw"] } 41 | uuid = { version = "0.8.1", features = ["v4"] } 42 | ws = "0.9.1" 43 | actix-session = "0.4.0" 44 | reqwest = { version = "0.10.10", features = ["json"] } 45 | hex = "0.4.3" 46 | url = "2.2.1" 47 | 48 | [package.metadata.deb] 49 | name = "jielabs-backend" 50 | assets = [ 51 | ["target/release/backend", "usr/bin/jielabs_backend", "755"], 52 | ["target/release/mock_user", "usr/bin/jielabs_mock_user", "755"], 53 | ["target/release/mock_board", "usr/bin/jielabs_mock_board", "755"], 54 | ["jielabs-backend.service", "lib/systemd/system/", "644"] 55 | ] 56 | section = "web" 57 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.50.0-slim AS builder 2 | RUN sed -i -e 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' -e 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list 3 | RUN apt update 4 | RUN apt install -y libssl-dev pkg-config libpq-dev 5 | 6 | RUN USER=root cargo new --bin backend 7 | WORKDIR /backend 8 | COPY ./Cargo.lock . 9 | COPY ./Cargo.toml . 10 | COPY ./.cargo .cargo 11 | RUN cargo build --release 12 | RUN rm -rf src 13 | 14 | COPY . . 15 | RUN cargo build --release 16 | 17 | FROM rust:1.50.0-slim 18 | RUN sed -i -e 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' -e 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list 19 | RUN apt update 20 | RUN apt install -y libssl-dev pkg-config libpq-dev 21 | COPY --from=builder /backend/target/release/backend /bin/backend 22 | CMD /bin/backend 23 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend for JieLabs 2 | 3 | ## Installation 4 | 5 | Install rustup, cargo, postgresql and redis. Then, install `cargo-deb` add user for jielabs: 6 | 7 | ```shell 8 | cargo install cargo-deb 9 | useradd -m jielabs 10 | ``` 11 | 12 | Then, use `cargo-deb` to install backend: 13 | 14 | ```shell 15 | cargo deb --install 16 | ``` 17 | 18 | Then, put a `.env` file under `/srv/jielabsweb-backend/`, chown to `jielabs`, and start the systemd service `jielabs-backend.service`. 19 | 20 | ## Setup telegraf 21 | 22 | Add following lines to `/etc/telegraf.conf`: 23 | 24 | ``` 25 | [[inputs.http]] 26 | urls = ["http://localhost:8080/api/metric/"] 27 | headers = {"Authorization" = "Bearer $METRIC_AUTH"} 28 | data_format = "influx" 29 | ``` -------------------------------------------------------------------------------- /backend/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # use with: DOCKER_ARGS="--build-arg https_proxy=proxy" REGISTRY=host:port ./build.sh 3 | set -e 4 | 5 | docker pull $REGISTRY/thu-cs-lab/jielabs-backend:builder || true 6 | docker build --target builder --cache-from $REGISTRY/thu-cs-lab/jielabs-backend:builder $DOCKER_ARGS -t thu-cs-lab/jielabs-backend:builder . 7 | docker tag thu-cs-lab/jielabs-backend:builder $REGISTRY/thu-cs-lab/jielabs-backend:builder 8 | docker push $REGISTRY/thu-cs-lab/jielabs-backend:builder 9 | docker pull $REGISTRY/thu-cs-lab/jielabs-backend:latest || true 10 | docker build --cache-from $REGISTRY/thu-cs-lab/jielabs-backend:builder --cache-from $REGISTRY/thu-cs-lab/jielabs-backend:latest $DOCKER_ARGS -t thu-cs-lab/jielabs-backend:latest . 11 | docker tag thu-cs-lab/jielabs-backend:latest $REGISTRY/thu-cs-lab/jielabs-backend:latest 12 | docker push $REGISTRY/thu-cs-lab/jielabs-backend:latest 13 | -------------------------------------------------------------------------------- /backend/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | -------------------------------------------------------------------------------- /backend/jielabs-backend.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=JieLabs Backend 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Environment="RUST_LOG=info" 8 | WorkingDirectory=/srv/jielabsweb-backend 9 | User=jielabs 10 | Group=jielabs 11 | ExecStart=/usr/bin/jielabs_backend 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | 16 | -------------------------------------------------------------------------------- /backend/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/backend/migrations/.gitkeep -------------------------------------------------------------------------------- /backend/migrations/2020-02-12-120630_create_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users 2 | -------------------------------------------------------------------------------- /backend/migrations/2020-02-12-120630_create_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users( 2 | id SERIAL NOT NULL, 3 | user_name TEXT NOT NULL, 4 | password TEXT NOT NULL, 5 | real_name TEXT NULL, 6 | class TEXT NULL, 7 | student_id TEXT NULL, 8 | role TEXT NOT NULL, 9 | PRIMARY KEY (id), 10 | UNIQUE (user_name) 11 | ) -------------------------------------------------------------------------------- /backend/migrations/2020-02-13-144220_create_jobs/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE jobs -------------------------------------------------------------------------------- /backend/migrations/2020-02-13-144220_create_jobs/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE jobs ( 2 | id SERIAL NOT NULL, 3 | submitter TEXT NOT NULL, 4 | type TEXT NOT NULL, 5 | source TEXT NOT NULL, 6 | status TEXT, 7 | destination TEXT, 8 | metadata TEXT NOT NULL DEFAULT TEXT '{}', 9 | task_id TEXT, 10 | PRIMARY KEY (id), 11 | UNIQUE (task_id) 12 | ) 13 | -------------------------------------------------------------------------------- /backend/migrations/2020-02-16-121048_create_configs/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE configs -------------------------------------------------------------------------------- /backend/migrations/2020-02-16-121048_create_configs/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE configs ( 2 | id SERIAL NOT NULL, 3 | key TEXT NOT NULL, 4 | value TEXT, 5 | PRIMARY KEY (id), 6 | UNIQUE (key) 7 | ) -------------------------------------------------------------------------------- /backend/migrations/2020-02-23-082509_add_time_to_jobs/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE jobs DROP COLUMN created_at; 2 | ALTER TABLE jobs DROP COLUMN finished_at; -------------------------------------------------------------------------------- /backend/migrations/2020-02-23-082509_add_time_to_jobs/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE jobs ADD COLUMN created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP; 2 | ALTER TABLE jobs ADD COLUMN finished_at TIMESTAMP WITH TIME ZONE; -------------------------------------------------------------------------------- /backend/migrations/2020-03-07-000036_add_last_login_to_users/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN last_login; 2 | -------------------------------------------------------------------------------- /backend/migrations/2020-03-07-000036_add_last_login_to_users/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN last_login TIMESTAMP WITH TIME ZONE; -------------------------------------------------------------------------------- /backend/migrations/2021-03-16-124339_make_password_nullable/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ALTER COLUMN password SET NOT NULL; -------------------------------------------------------------------------------- /backend/migrations/2021-03-16-124339_make_password_nullable/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ALTER COLUMN password DROP NOT NULL; -------------------------------------------------------------------------------- /backend/src/bin/backend.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel_migrations; 3 | use actix_session::CookieSession; 4 | use actix_web::cookie::SameSite; 5 | use actix_web::http::header; 6 | use actix_web::{middleware, web, App, HttpServer}; 7 | use backend::{ 8 | board, env::ENV, file, metric, session, task, task_manager, user, ws_board, ws_user, 9 | DbConnection, 10 | }; 11 | use diesel::r2d2::{ConnectionManager, Pool}; 12 | use dotenv::dotenv; 13 | use log::*; 14 | use ring::{digest, rand}; 15 | use sentry; 16 | 17 | embed_migrations!(); 18 | 19 | #[actix_rt::main] 20 | async fn main() -> std::io::Result<()> { 21 | dotenv().ok(); 22 | env_logger::init(); 23 | 24 | let _guard = if let Some(url) = ENV.sentry_url.clone() { 25 | info!("Sentry report is up"); 26 | Some(sentry::init(( 27 | url, 28 | sentry::ClientOptions { 29 | release: sentry::release_name!(), 30 | ..Default::default() 31 | }, 32 | ))) 33 | } else { 34 | None 35 | }; 36 | 37 | let conn = ENV.database_url.clone(); 38 | let manager = ConnectionManager::::new(conn); 39 | let pool = Pool::builder().build(manager).expect("create db pool"); 40 | let conn = pool.get().expect("get conn"); 41 | embedded_migrations::run_with_output(&conn, &mut std::io::stdout()).expect("migration"); 42 | drop(conn); 43 | 44 | task_manager::get_task_manager().do_send(task_manager::SetDb { db: pool.clone() }); 45 | 46 | let secret = ENV.cookie_secret.clone(); 47 | let secret = digest::digest(&digest::SHA512, secret.as_bytes()); 48 | HttpServer::new(move || { 49 | App::new() 50 | .data(pool.clone()) 51 | .data(rand::SystemRandom::new()) 52 | .wrap( 53 | actix_cors::Cors::default() 54 | .supports_credentials() 55 | .allowed_methods(vec!["GET", "POST"]) 56 | .allowed_headers(vec![header::CONTENT_TYPE, header::UPGRADE]) 57 | .allowed_origin("http://localhost:3000") 58 | .allowed_origin("https://lab.cs.tsinghua.edu.cn"), 59 | ) 60 | .wrap( 61 | CookieSession::private(secret.as_ref()) // Private is required because we are storing OAuth state in cookie 62 | .name("jielabsweb-rich") 63 | .path(&ENV.cookie_path) 64 | .secure(!cfg!(debug_assertions)) 65 | .same_site(SameSite::None), 66 | ) 67 | .wrap(middleware::Logger::new( 68 | r#"%a %{r}a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#, // add real ip for reverse proxy 69 | )) 70 | .service( 71 | web::scope(&ENV.api_root) 72 | .service(web::resource("/ws_board").route(web::get().to(ws_board::ws_board))) 73 | .service(web::resource("/ws_user").route(web::get().to(ws_user::ws_user))) 74 | .service( 75 | web::scope("/user") 76 | .service(user::list) 77 | .service(user::count) 78 | .service(user::update) 79 | .service(user::create) 80 | .service(user::remove), 81 | ) 82 | .service(web::scope("/file").service(file::upload)) 83 | .service( 84 | web::scope("/board") 85 | .service(board::list) 86 | .service(board::config_board) 87 | .service(board::get_version) 88 | .service(board::update_version), 89 | ) 90 | .service( 91 | web::scope("/task") 92 | .service(task::build) 93 | .service(task::finish) 94 | .service(task::get) 95 | .service(task::list) 96 | .service(task::count) 97 | .service(task::list_self), 98 | ) 99 | .service(web::scope("/metric").service(metric::get)) 100 | .service( 101 | web::scope("/") 102 | .service(session::login) 103 | .service(session::logout) 104 | .service(session::info) 105 | .service(session::portal_fwd) 106 | .service(session::portal_cb), 107 | ), 108 | ) 109 | }) 110 | .bind(&std::env::var("LISTEN").unwrap_or("127.0.0.1:8080".to_string()))? 111 | .run() 112 | .await 113 | } 114 | -------------------------------------------------------------------------------- /backend/src/bin/create_user.rs: -------------------------------------------------------------------------------- 1 | use backend; 2 | use diesel::prelude::*; 3 | use dotenv::dotenv; 4 | use structopt::StructOpt; 5 | 6 | #[derive(StructOpt)] 7 | struct Args { 8 | #[structopt(short, long)] 9 | user_name: String, 10 | 11 | #[structopt(short, long)] 12 | password: String, 13 | 14 | #[structopt(short, long)] 15 | real_name: Option, 16 | 17 | #[structopt(short, long)] 18 | student_id: Option, 19 | 20 | #[structopt(short, long)] 21 | class: Option, 22 | 23 | #[structopt(short = "R", long)] 24 | role: Option, 25 | } 26 | 27 | #[paw::main] 28 | fn main(args: Args) { 29 | dotenv().ok(); 30 | let url = backend::env::ENV.database_url.clone(); 31 | let conn = backend::DbConnection::establish(&url).expect("connect"); 32 | 33 | let new_user = backend::models::NewUser { 34 | user_name: args.user_name, 35 | password: Some(backend::session::hash_password(&args.password)), 36 | real_name: args.real_name, 37 | student_id: args.student_id, 38 | class: args.class, 39 | role: args.role.unwrap_or(String::from("user")), 40 | last_login: None, 41 | }; 42 | diesel::insert_into(backend::schema::users::table) 43 | .values(&new_user) 44 | .execute(&conn) 45 | .expect("insert should not fail"); 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/bin/create_users.rs: -------------------------------------------------------------------------------- 1 | use backend; 2 | use diesel::prelude::*; 3 | use dotenv::dotenv; 4 | use rand::distributions::Alphanumeric; 5 | use rand::{thread_rng, Rng}; 6 | use serde::{Deserialize, Serialize}; 7 | use std::fs::File; 8 | use structopt::StructOpt; 9 | 10 | #[derive(StructOpt)] 11 | struct Args { 12 | #[structopt(short, long)] 13 | input_csv: String, 14 | 15 | #[structopt(short, long)] 16 | output_csv: String, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | struct Row { 21 | user_name: String, 22 | password: Option, 23 | real_name: Option, 24 | student_id: Option, 25 | class: Option, 26 | role: Option, 27 | } 28 | 29 | #[paw::main] 30 | fn main(args: Args) { 31 | dotenv().ok(); 32 | let url = backend::env::ENV.database_url.clone(); 33 | let conn = backend::DbConnection::establish(&url).expect("connect"); 34 | 35 | let input = File::open(args.input_csv).unwrap(); 36 | let output = File::create(args.output_csv).unwrap(); 37 | let mut rdr = csv::Reader::from_reader(input); 38 | let mut wtr = csv::Writer::from_writer(output); 39 | conn.transaction::<_, diesel::result::Error, _>(|| { 40 | for result in rdr.deserialize() { 41 | let mut record: Row = result.unwrap(); 42 | println!("adding {:?}", record); 43 | let password = record.password.unwrap_or_else(|| { 44 | String::from_utf8( 45 | thread_rng() 46 | .sample_iter(&Alphanumeric) 47 | .take(10) 48 | .collect::>(), 49 | ) 50 | .unwrap() 51 | }); 52 | record.password = Some(password.clone()); 53 | wtr.serialize(record.clone()).unwrap(); 54 | 55 | let new_user = backend::models::NewUser { 56 | user_name: record.user_name, 57 | password: Some(backend::session::hash_password(&password)), 58 | real_name: record.real_name, 59 | student_id: record.student_id, 60 | class: record.class, 61 | role: record.role.unwrap_or(String::from("user")), 62 | last_login: None, 63 | }; 64 | diesel::insert_into(backend::schema::users::table) 65 | .values(&new_user) 66 | .execute(&conn)?; 67 | } 68 | Ok(()) 69 | }) 70 | .expect("run transaction"); 71 | println!("All users added"); 72 | } 73 | -------------------------------------------------------------------------------- /backend/src/bin/fix_jobs.rs: -------------------------------------------------------------------------------- 1 | use backend; 2 | use diesel::prelude::*; 3 | use dotenv::dotenv; 4 | 5 | use structopt::StructOpt; 6 | 7 | #[derive(StructOpt)] 8 | struct Args { 9 | #[structopt(short, long)] 10 | id: i32, 11 | } 12 | 13 | #[paw::main] 14 | fn main(args: Args) { 15 | dotenv().ok(); 16 | let url = backend::env::ENV.database_url.clone(); 17 | let db_conn = backend::DbConnection::establish(&url).expect("connect"); 18 | let client = redis::Client::open(backend::env::ENV.redis_url.clone()).expect("redis client"); 19 | let mut conn = client.get_connection().unwrap(); 20 | 21 | let job = backend::schema::jobs::dsl::jobs 22 | .filter(backend::schema::jobs::dsl::id.eq(args.id)) 23 | .first::(&db_conn) 24 | .unwrap(); 25 | if job.status.is_none() { 26 | let src_url = backend::common::get_download_url(&job.source); 27 | let dst_url = backend::common::get_upload_url(&job.destination.unwrap()); 28 | let req = backend::task_manager::SubmitBuildTask { 29 | id: job.task_id.clone().unwrap(), 30 | src: src_url, 31 | dst: dst_url, 32 | timestamp: backend::common::get_timestamp(), 33 | }; 34 | redis::cmd("LPUSH") 35 | .arg(&backend::env::ENV.redis_waiting_queue) 36 | .arg(serde_json::to_string(&req).expect("to json")) 37 | .execute(&mut conn); 38 | println!("Added task {:?}", job.task_id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/bin/generate_hash.rs: -------------------------------------------------------------------------------- 1 | use backend; 2 | use dotenv::dotenv; 3 | use structopt::StructOpt; 4 | 5 | #[derive(StructOpt)] 6 | struct Args { 7 | #[structopt(short, long)] 8 | password: String, 9 | } 10 | 11 | #[paw::main] 12 | fn main(args: Args) { 13 | dotenv().ok(); 14 | println!("hash: {}", backend::session::hash_password(&args.password)); 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/bin/mock_board.rs: -------------------------------------------------------------------------------- 1 | use backend::common::IOSetting; 2 | use backend::ws_board; 3 | use env_logger; 4 | use serde_json; 5 | use std::sync::{Arc, Mutex}; 6 | use structopt::StructOpt; 7 | use ws::connect; 8 | 9 | #[derive(StructOpt, Clone)] 10 | struct Args { 11 | #[structopt(short, long, default_value = "127.0.0.1:8080")] 12 | host: String, 13 | 14 | #[structopt(short, long, default_value = "100")] 15 | step_time_ms: u64, 16 | 17 | #[structopt(short, long, default_value = "password")] 18 | password: String, 19 | } 20 | 21 | #[paw::main] 22 | fn main(args: Args) { 23 | env_logger::init(); 24 | let step_time = args.step_time_ms; 25 | connect(format!("ws://{}/api/ws_board", args.host), |out| { 26 | out.send(format!("{}{}{}", r#"{"Authenticate":{"password":""#, args.password, r#"","software_version":"1.0","hardware_version":"0.1"}}"#)).unwrap(); 27 | let spawned = Arc::new(Mutex::new(false)); 28 | move |msg| { 29 | println!("Client got message '{}'. ", msg); 30 | match msg { 31 | ws::Message::Text(text) => { 32 | if let Ok(msg) = serde_json::from_str::(&text) { 33 | match msg { 34 | ws_board::WSBoardMessageS2B::SetIODirection(direction) => { 35 | println!("Set IO Direction {:?}", direction); 36 | out.send(serde_json::to_string(&ws_board::WSBoardMessageB2S::ReportIOChange(IOSetting { 37 | mask: None, 38 | data: Some(String::from("1111")), 39 | })).unwrap()).unwrap(); 40 | } 41 | ws_board::WSBoardMessageS2B::SetIOOutput(output) => { 42 | println!("Set IO Output {:?}", output); 43 | out.send(serde_json::to_string(&ws_board::WSBoardMessageB2S::ReportIOChange(IOSetting { 44 | mask: None, 45 | data: Some(String::from("1111")), 46 | })).unwrap()).unwrap(); 47 | } 48 | ws_board::WSBoardMessageS2B::SubscribeIOChange(_) => { 49 | println!("Subscribe to io change"); 50 | 51 | let mut check = spawned.lock().unwrap(); 52 | if !*check { 53 | *check = true; 54 | let sender = out.clone(); 55 | std::thread::spawn(move || { 56 | let mut shift = 0; 57 | loop { 58 | let data = (0..64).map(|_| "0").collect::(); 59 | *data.chars().nth(shift).as_mut().unwrap() = '1'; 60 | sender.send(serde_json::to_string(&ws_board::WSBoardMessageB2S::ReportIOChange(IOSetting { 61 | mask: None, 62 | data: Some(data), 63 | })).unwrap()).unwrap(); 64 | std::thread::sleep(std::time::Duration::from_millis(step_time)); 65 | shift = (shift + 1) % 40; 66 | } 67 | }); 68 | } 69 | } 70 | ws_board::WSBoardMessageS2B::UnsubscribeIOChange(_) => { 71 | println!("Unsubscribe to io change"); 72 | } 73 | _ => {} // Clocking related 74 | } 75 | } 76 | } 77 | ws::Message::Binary(bit) => { 78 | println!("Got bitstream of length {}", bit.len()); 79 | out.send(serde_json::to_string(&ws_board::WSBoardMessageB2S::ProgramBitstreamFinish(true)).unwrap()).unwrap(); 80 | } 81 | } 82 | Ok(()) 83 | } 84 | }).unwrap(); 85 | } 86 | -------------------------------------------------------------------------------- /backend/src/bin/mock_user.rs: -------------------------------------------------------------------------------- 1 | use backend::ws_user; 2 | use env_logger; 3 | use log::*; 4 | use serde_json; 5 | use std::fmt::Write; 6 | use std::fs::File; 7 | use std::io::Read; 8 | use structopt::StructOpt; 9 | use ws::connect; 10 | 11 | #[derive(StructOpt, Clone)] 12 | struct Args { 13 | #[structopt(short, long, default_value = "127.0.0.1:8080")] 14 | host: String, 15 | 16 | #[structopt(short, long)] 17 | bitstream: Option, 18 | } 19 | 20 | #[paw::main] 21 | fn main(args: Args) { 22 | env_logger::init(); 23 | connect(format!("ws://{}/api/ws_user", args.host), |out| { 24 | out.send(r#"{"RequestForBoard":""}"#).unwrap(); 25 | let bitstream = args.bitstream.clone(); 26 | move |msg| { 27 | println!("Client got message '{}'. ", msg); 28 | if let ws::Message::Text(text) = msg { 29 | if let Ok(msg) = serde_json::from_str::(&text) { 30 | match msg { 31 | ws_user::WSUserMessageS2U::BoardAllocateResult(res) => { 32 | println!("Board allocation result: {:?}", res); 33 | if res.is_some() { 34 | out.send(r#"{"ToBoard":{"SetIOOutput":{"mask":"","data":""}}}"#) 35 | .unwrap(); 36 | info!("SetIOOutput sent"); 37 | out.send(r#"{"ToBoard":{"SetIODirection":{"mask":"","data":""}}}"#) 38 | .unwrap(); 39 | info!("SetIODirection sent"); 40 | out.send(r#"{"ToBoard":{"SubscribeIOChange":""}}"#).unwrap(); 41 | info!("SubscribeIOChange sent"); 42 | if let Some(bitstream_path) = bitstream.clone() { 43 | let mut file = File::open(bitstream_path).unwrap(); 44 | let mut data = vec![]; 45 | file.read_to_end(&mut data).unwrap(); 46 | let mut s = String::new(); 47 | for &byte in &data { 48 | write!(&mut s, "{:02X}", byte).expect("Unable to write"); 49 | } 50 | 51 | out.send(format!( 52 | r#"{{"ToBoard":{{"ProgramBitstream":"{}"}}}}"#, 53 | s 54 | )) 55 | .unwrap(); 56 | info!("ProgramBitstream sent"); 57 | } 58 | } 59 | } 60 | ws_user::WSUserMessageS2U::ReportIOChange(change) => { 61 | println!("IO changed {:?}", change); 62 | } 63 | ws_user::WSUserMessageS2U::BoardDisconnected(_) => { 64 | println!("Board disconnected"); 65 | } 66 | ws_user::WSUserMessageS2U::ProgramBitstreamFinish(result) => { 67 | println!("Program bitstream finished with {}", result); 68 | } 69 | } 70 | } 71 | } 72 | Ok(()) 73 | } 74 | }) 75 | .unwrap(); 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/bin/s3_gc.rs: -------------------------------------------------------------------------------- 1 | use backend::common::*; 2 | use backend::env::ENV; 3 | use backend::models::*; 4 | use backend::schema::jobs; 5 | use backend::DbConnection; 6 | use diesel::prelude::*; 7 | use diesel::r2d2::{ConnectionManager, Pool}; 8 | use dotenv::dotenv; 9 | use rusoto_s3::{ListObjectsRequest, S3}; 10 | use std::collections::BTreeSet; 11 | 12 | #[actix_rt::main] 13 | async fn main() -> std::io::Result<()> { 14 | dotenv().ok(); 15 | 16 | let url = backend::env::ENV.database_url.clone(); 17 | let manager = ConnectionManager::::new(url); 18 | let pool = Pool::builder().build(manager).expect("create db pool"); 19 | let conn = pool.get().expect("get conn"); 20 | let jobs = jobs::dsl::jobs.load::(&conn).unwrap(); 21 | let mut id = BTreeSet::new(); 22 | for job in &jobs { 23 | id.insert(job.source.clone()); 24 | if let Some(dest) = &job.destination { 25 | id.insert(dest.clone()); 26 | } 27 | } 28 | println!("Live: {} files", id.len()); 29 | 30 | let client = s3_client(); 31 | let bucket = ENV.s3_bucket.clone(); 32 | let req = ListObjectsRequest { 33 | bucket, 34 | ..Default::default() 35 | }; 36 | let res = client.list_objects(req).await.unwrap(); 37 | let mut keys = BTreeSet::new(); 38 | for object in &res.contents.unwrap() { 39 | if let Some(key) = &object.key { 40 | keys.insert(key.clone()); 41 | } 42 | } 43 | println!("All: {} files", keys.len()); 44 | let diff: Vec<_> = keys.difference(&id).cloned().collect(); 45 | println!("GC'ed: {} files", diff.len()); 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/board.rs: -------------------------------------------------------------------------------- 1 | use crate::board_manager::{get_board_manager, GetBoardList, SendToBoardByRemote}; 2 | use crate::common::err; 3 | use crate::schema::configs; 4 | use crate::session::get_user; 5 | use crate::ws_board::WSBoardMessageS2B; 6 | use crate::DbPool; 7 | use actix_session::Session; 8 | use actix_web::{get, post, web, HttpResponse, Result}; 9 | use diesel::prelude::*; 10 | use serde_derive::{Deserialize, Serialize}; 11 | 12 | #[get("/list")] 13 | async fn list(sess: Session, pool: web::Data) -> Result { 14 | let conn = pool.get().map_err(err)?; 15 | if let (Some(user), _conn) = get_user(&sess, conn).await? { 16 | if user.role == "admin" { 17 | let man = get_board_manager(); 18 | if let Ok(res) = man.send(GetBoardList).await { 19 | return Ok(HttpResponse::Ok().json(res.0)); 20 | } 21 | } 22 | } 23 | Ok(HttpResponse::Forbidden().finish()) 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | struct UpdateVersionRequest { 28 | version: String, 29 | url: String, 30 | hash: String, 31 | } 32 | 33 | #[post("/version")] 34 | async fn update_version( 35 | sess: Session, 36 | pool: web::Data, 37 | body: web::Json, 38 | ) -> Result { 39 | let conn = pool.get().map_err(err)?; 40 | if let (Some(user), conn) = get_user(&sess, conn).await? { 41 | if user.role == "admin" { 42 | let body = serde_json::to_string(&*body)?; 43 | web::block(move || { 44 | let kv = ( 45 | configs::dsl::key.eq("version"), 46 | configs::dsl::value.eq(&body), 47 | ); 48 | diesel::insert_into(configs::table) 49 | .values(kv) 50 | .on_conflict(configs::dsl::key) 51 | .do_update() 52 | .set(kv) 53 | .execute(&conn) 54 | }) 55 | .await 56 | .map_err(err)?; 57 | return Ok(HttpResponse::Ok().json(true)); 58 | } 59 | } 60 | Ok(HttpResponse::Forbidden().finish()) 61 | } 62 | 63 | #[get("/version")] 64 | async fn get_version(pool: web::Data) -> Result { 65 | let conn = pool.get().map_err(err)?; 66 | let config = web::block(move || { 67 | configs::dsl::configs 68 | .select(configs::dsl::value) 69 | .filter(configs::dsl::key.eq("version")) 70 | .first::>(&conn) 71 | .optional() 72 | }) 73 | .await 74 | .map_err(err)?; 75 | if let Some(res) = config { 76 | if let Some(body) = res { 77 | let info: UpdateVersionRequest = serde_json::from_str(&body)?; 78 | let body = format!("{}\n{}\n{}\n", info.version, info.url, info.hash); 79 | return Ok(HttpResponse::Ok().body(&body)); 80 | } 81 | } else { 82 | // unset 83 | return Ok(HttpResponse::Ok().body("\n\n\n")); 84 | } 85 | 86 | Ok(HttpResponse::Forbidden().finish()) 87 | } 88 | 89 | #[derive(Serialize, Deserialize)] 90 | struct ConfigBoardRequest { 91 | board: String, 92 | ident: bool, 93 | } 94 | 95 | #[post("/config")] 96 | async fn config_board( 97 | sess: Session, 98 | pool: web::Data, 99 | body: web::Json, 100 | ) -> Result { 101 | let conn = pool.get().map_err(err)?; 102 | if let (Some(user), _conn) = get_user(&sess, conn).await? { 103 | if user.role == "admin" { 104 | let res = get_board_manager() 105 | .send(SendToBoardByRemote { 106 | remote: body.board.clone(), 107 | action: WSBoardMessageS2B::Ident(body.ident), 108 | }) 109 | .await 110 | .map_err(err)?; 111 | return Ok(HttpResponse::Ok().json(res)); 112 | } 113 | } 114 | Ok(HttpResponse::Forbidden().finish()) 115 | } 116 | -------------------------------------------------------------------------------- /backend/src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::env::ENV; 2 | use actix_web::{error::ErrorInternalServerError, Error}; 3 | use bytes::Bytes; 4 | use futures::TryStreamExt; 5 | use log::*; 6 | use rusoto_core::credential::{AwsCredentials, StaticProvider}; 7 | use rusoto_core::Region; 8 | use rusoto_s3::util::{PreSignedRequest, PreSignedRequestOption}; 9 | use rusoto_s3::S3; 10 | use rusoto_s3::{GetObjectRequest, PutObjectRequest, S3Client}; 11 | use serde_derive::{Deserialize, Serialize}; 12 | use std::fmt::Display; 13 | use std::time::{Duration, SystemTime}; 14 | use uuid::Uuid; 15 | 16 | #[derive(Serialize, Deserialize, Debug)] 17 | pub struct IOSetting { 18 | pub mask: Option, 19 | pub data: Option, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug)] 23 | pub struct ClockSetting { 24 | pub frequency: u32, 25 | } 26 | 27 | pub fn get_timestamp() -> u64 { 28 | SystemTime::now() 29 | .duration_since(SystemTime::UNIX_EPOCH) 30 | .unwrap() 31 | .as_secs() 32 | } 33 | 34 | pub fn generate_uuid() -> String { 35 | let uuid = Uuid::new_v4(); 36 | uuid.to_simple() 37 | .encode_lower(&mut Uuid::encode_buffer()) 38 | .to_owned() 39 | } 40 | 41 | pub fn s3_credentials() -> AwsCredentials { 42 | AwsCredentials::new(ENV.s3_key.clone(), ENV.s3_secret.clone(), None, None) 43 | } 44 | 45 | pub fn s3_client() -> S3Client { 46 | let client = S3Client::new_with( 47 | rusoto_core::request::HttpClient::new().expect("Failed to creat HTTP client"), 48 | StaticProvider::from(s3_credentials()), 49 | s3_region(), 50 | ); 51 | client 52 | } 53 | 54 | pub fn s3_region() -> Region { 55 | Region::Custom { 56 | name: ENV.s3_region.clone(), 57 | endpoint: ENV.s3_endpoint.clone(), 58 | } 59 | } 60 | 61 | pub fn get_upload_url(file_name: &String) -> String { 62 | let bucket = ENV.s3_bucket.clone(); 63 | let req = PutObjectRequest { 64 | bucket, 65 | key: file_name.clone(), 66 | ..Default::default() 67 | }; 68 | let option = PreSignedRequestOption { 69 | expires_in: Duration::from_secs(3600 * 24), 70 | }; 71 | let presigned_url = req.get_presigned_url(&s3_region(), &s3_credentials(), &option); 72 | presigned_url 73 | } 74 | 75 | pub fn get_download_url(file_name: &String) -> String { 76 | let bucket = ENV.s3_bucket.clone(); 77 | let req = GetObjectRequest { 78 | bucket, 79 | key: file_name.clone(), 80 | ..Default::default() 81 | }; 82 | let option = PreSignedRequestOption { 83 | expires_in: Duration::from_secs(3600 * 24), 84 | }; 85 | let presigned_url = req.get_presigned_url(&s3_region(), &s3_credentials(), &option); 86 | presigned_url 87 | } 88 | 89 | pub async fn download_s3(file_name: String) -> Option { 90 | let bucket = ENV.s3_bucket.clone(); 91 | let client = s3_client(); 92 | let req = GetObjectRequest { 93 | bucket, 94 | key: file_name.clone(), 95 | ..Default::default() 96 | }; 97 | if let Ok(result) = client.get_object(req).await { 98 | if let Some(stream) = result.body { 99 | if let Ok(body) = stream 100 | .map_ok(|b| bytes::BytesMut::from(&b[..])) 101 | .try_concat() 102 | .await 103 | { 104 | return Some(body.freeze()); 105 | } 106 | } 107 | } 108 | None 109 | } 110 | 111 | #[track_caller] 112 | pub fn err(err: T) -> Error { 113 | let error_token = generate_uuid(); 114 | let location = std::panic::Location::caller(); 115 | error!("Error {} at {}: {}", error_token, location, err); 116 | sentry::capture_message( 117 | &format!( 118 | "token: {}, err: {}, location: {}", 119 | error_token, err, location 120 | ), 121 | sentry::Level::Error, 122 | ); 123 | ErrorInternalServerError(format!( 124 | "Please contact admin with error token {}", 125 | error_token 126 | )) 127 | } 128 | -------------------------------------------------------------------------------- /backend/src/env.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::env::var; 3 | 4 | pub struct Env { 5 | pub cookie_path: String, 6 | pub api_root: String, 7 | pub database_url: String, 8 | pub cookie_secret: String, 9 | pub password_secret: String, 10 | pub board_pass: String, 11 | pub metric_auth: String, 12 | pub allow_anonymous_ws_user: bool, 13 | // s3 14 | pub s3_endpoint: String, 15 | pub s3_bucket: String, 16 | pub s3_key: String, 17 | pub s3_secret: String, 18 | pub s3_region: String, 19 | // redis 20 | pub redis_url: String, 21 | pub redis_waiting_queue: String, 22 | pub redis_working_queue: String, 23 | // sentry 24 | pub sentry_url: Option, 25 | // portal 26 | pub portal: String, 27 | pub portal_client_id: String, 28 | pub portal_client_secret: String, 29 | pub base: String, 30 | } 31 | 32 | fn get_env() -> Env { 33 | Env { 34 | cookie_path: var("COOKIE_PATH").unwrap_or(String::from("/")), 35 | api_root: var("API_ROOT").unwrap_or(String::from("/api")), 36 | database_url: var("DATABASE_URL").expect("DATABASE_URL"), 37 | cookie_secret: var("COOKIE_SECRET").expect("COOKIE_SECRET"), 38 | password_secret: var("PASSWORD_SECRET").expect("PASSWORD_SECRET"), 39 | board_pass: var("BOARD_PASS").expect("BOARD_PASS"), 40 | metric_auth: var("METRIC_AUTH").expect("METRIC_AUTH"), 41 | allow_anonymous_ws_user: var("ALLOW_ANONYMOUS_WS_USER").is_ok(), 42 | s3_endpoint: var("S3_ENDPOINT").expect("S3_ENDPOINT"), 43 | s3_bucket: var("S3_BUCKET").expect("S3_BUCKET"), 44 | s3_key: var("S3_KEY").expect("S3_KEY"), 45 | s3_secret: var("S3_SECRET").expect("S3_SECRET"), 46 | s3_region: var("S3_REGION").expect("S3_REGION"), 47 | redis_url: var("REDIS_URL").expect("REDIS_URL"), 48 | redis_waiting_queue: var("REDIS_WAITING_QUEUE").expect("jielabs-waiting"), 49 | redis_working_queue: var("REDIS_WORKING_QUEUE").expect("jielabs-working"), 50 | sentry_url: var("SENTRY_URL").ok(), 51 | portal: var("PORTAL") 52 | .unwrap_or_else(|_| "https://lab.cs.tsinghua.edu.cn/portal".to_owned()), 53 | portal_client_id: var("PORTAL_CLIENT_ID").unwrap_or_else(|_| "jielabs".to_owned()), 54 | portal_client_secret: var("PORTAL_CLIENT_SECRET").expect("PORTAL_CLIENT_SECRET"), 55 | base: var("BASE").unwrap_or_else(|_| "https://lab.cs.tsinghua.edu.cn/jie".to_owned()), 56 | } 57 | } 58 | 59 | lazy_static! { 60 | pub static ref ENV: Env = get_env(); 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/file.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{err, generate_uuid, get_upload_url}; 2 | use crate::session::get_user; 3 | use crate::DbPool; 4 | use actix_session::Session; 5 | use actix_web::{get, web, HttpResponse, Result}; 6 | use serde_derive::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | struct UploadResponse { 10 | uuid: String, 11 | url: String, 12 | } 13 | 14 | #[get("/upload")] 15 | async fn upload(sess: Session, pool: web::Data) -> Result { 16 | let conn = pool.get().map_err(err)?; 17 | if let (Some(_user), _conn) = get_user(&sess, conn).await? { 18 | let file_name = generate_uuid(); 19 | let presigned_url = get_upload_url(&file_name); 20 | return Ok(HttpResponse::Ok().json(UploadResponse { 21 | uuid: file_name, 22 | url: presigned_url, 23 | })); 24 | } 25 | Ok(HttpResponse::Forbidden().finish()) 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | use diesel::prelude::*; 5 | use diesel::r2d2::{ConnectionManager, Pool}; 6 | 7 | pub type DbConnection = PgConnection; 8 | type DbPool = Pool>; 9 | 10 | pub mod board; 11 | pub mod board_manager; 12 | pub mod common; 13 | pub mod env; 14 | pub mod file; 15 | pub mod metric; 16 | pub mod models; 17 | pub mod schema; 18 | pub mod session; 19 | pub mod task; 20 | pub mod task_manager; 21 | pub mod user; 22 | pub mod ws_board; 23 | pub mod ws_user; 24 | -------------------------------------------------------------------------------- /backend/src/metric.rs: -------------------------------------------------------------------------------- 1 | use crate::board_manager::{get_board_manager, BoardInfoList, GetBoardList}; 2 | use crate::common::err; 3 | use crate::env::ENV; 4 | use crate::schema::{jobs, users}; 5 | use crate::task_manager::{get_task_manager, GetMetric, GetMetricResponse}; 6 | use crate::ws_user::ONLINE_USERS; 7 | use crate::DbPool; 8 | use actix_web::{get, web, Result}; 9 | use actix_web_httpauth::extractors::bearer::BearerAuth; 10 | use diesel::prelude::*; 11 | use std::time::SystemTime; 12 | 13 | #[get("/")] 14 | async fn get(pool: web::Data, auth: BearerAuth) -> Result { 15 | if auth.token() == ENV.metric_auth { 16 | let timestamp = SystemTime::now() 17 | .duration_since(SystemTime::UNIX_EPOCH) 18 | .unwrap() 19 | .as_nanos(); 20 | let conn = pool.get().map_err(err)?; 21 | let user_count = users::dsl::users 22 | .count() 23 | .get_result::(&conn) 24 | .map_err(err)?; 25 | let live_user_count = users::dsl::users 26 | .filter(users::dsl::last_login.is_not_null()) 27 | .count() 28 | .get_result::(&conn) 29 | .map_err(err)?; 30 | let job_count = jobs::dsl::jobs 31 | .count() 32 | .get_result::(&conn) 33 | .map_err(err)?; 34 | let job_compilation_success_count = jobs::dsl::jobs 35 | .filter(jobs::dsl::status.eq("Compilation Success")) 36 | .count() 37 | .get_result::(&conn) 38 | .map_err(err)?; 39 | let job_compilation_failed_count = jobs::dsl::jobs 40 | .filter(jobs::dsl::status.eq("Compilation Failed")) 41 | .count() 42 | .get_result::(&conn) 43 | .map_err(err)?; 44 | let job_system_error_count = jobs::dsl::jobs 45 | .filter(jobs::dsl::status.eq("System Error")) 46 | .count() 47 | .get_result::(&conn) 48 | .map_err(err)?; 49 | let tasks: GetMetricResponse = get_task_manager().send(GetMetric).await.map_err(err)?; 50 | let boards: BoardInfoList = get_board_manager().send(GetBoardList).await.map_err(err)?; 51 | let board_count = boards.0.len(); 52 | let assigned_board_count = boards 53 | .0 54 | .iter() 55 | .filter(|board| board.connected_user.is_some()) 56 | .count(); 57 | Ok(format!( 58 | "jielabsweb-backend user-count={}i,live-user-count={}i,online-user-count={}i,job-count={}i,job-compilation-success-count={}i,job-compilation-failed-count={}i,job-system-error-count={}i,waiting-len={}i,working-len={}i,board-count={}i,assigned-board-count={}i {}", 59 | user_count, live_user_count, ONLINE_USERS.lock().unwrap().len(), job_count, job_compilation_success_count, job_compilation_failed_count, job_system_error_count, tasks.len_waiting, tasks.len_working, board_count, assigned_board_count, timestamp 60 | )) 61 | } else { 62 | Ok(format!("")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/models.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::{jobs, users}; 2 | use chrono::{DateTime, Utc}; 3 | 4 | #[derive(Debug, Queryable, AsChangeset, Identifiable)] 5 | pub struct User { 6 | pub id: i32, 7 | pub user_name: String, 8 | pub password: Option, 9 | pub real_name: Option, 10 | pub class: Option, 11 | pub student_id: Option, 12 | pub role: String, 13 | pub last_login: Option>, 14 | } 15 | 16 | #[derive(Debug, Insertable)] 17 | #[table_name = "users"] 18 | pub struct NewUser { 19 | pub user_name: String, 20 | pub password: Option, 21 | pub real_name: Option, 22 | pub class: Option, 23 | pub student_id: Option, 24 | pub role: String, 25 | pub last_login: Option>, 26 | } 27 | 28 | #[derive(Debug, Queryable, AsChangeset, Identifiable)] 29 | pub struct Job { 30 | pub id: i32, 31 | pub submitter: String, 32 | pub type_: String, 33 | pub source: String, 34 | pub status: Option, 35 | pub destination: Option, 36 | pub metadata: String, 37 | pub task_id: Option, 38 | pub created_at: DateTime, 39 | pub finished_at: Option>, 40 | } 41 | 42 | #[derive(Debug, Insertable)] 43 | #[table_name = "jobs"] 44 | pub struct NewJob { 45 | pub submitter: String, 46 | pub type_: String, 47 | pub source: String, 48 | pub status: Option, 49 | pub destination: Option, 50 | pub metadata: String, 51 | pub task_id: Option, 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | configs (id) { 3 | id -> Int4, 4 | key -> Text, 5 | value -> Nullable, 6 | } 7 | } 8 | 9 | table! { 10 | jobs (id) { 11 | id -> Int4, 12 | submitter -> Text, 13 | #[sql_name = "type"] 14 | type_ -> Text, 15 | source -> Text, 16 | status -> Nullable, 17 | destination -> Nullable, 18 | metadata -> Text, 19 | task_id -> Nullable, 20 | created_at -> Timestamptz, 21 | finished_at -> Nullable, 22 | } 23 | } 24 | 25 | table! { 26 | users (id) { 27 | id -> Int4, 28 | user_name -> Text, 29 | password -> Nullable, 30 | real_name -> Nullable, 31 | class -> Nullable, 32 | student_id -> Nullable, 33 | role -> Text, 34 | last_login -> Nullable, 35 | } 36 | } 37 | 38 | allow_tables_to_appear_in_same_query!(configs, jobs, users,); 39 | -------------------------------------------------------------------------------- /backend/src/task.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{err, generate_uuid, get_download_url, get_timestamp, get_upload_url}; 2 | use crate::models::*; 3 | use crate::schema::jobs; 4 | use crate::session::get_user; 5 | use crate::task_manager::{get_task_manager, SubmitBuildTask}; 6 | use crate::DbPool; 7 | use actix_session::Session; 8 | use actix_web::{get, post, web, HttpResponse, Result}; 9 | use chrono::{DateTime, Utc}; 10 | use diesel::prelude::*; 11 | use serde_derive::{Deserialize, Serialize}; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | struct BuildRequest { 15 | source: String, 16 | metadata: String, 17 | } 18 | 19 | #[post("/build")] 20 | async fn build( 21 | sess: Session, 22 | body: web::Json, 23 | pool: web::Data, 24 | ) -> Result { 25 | let conn = pool.get().map_err(err)?; 26 | if let (Some(user), conn) = get_user(&sess, conn).await? { 27 | let body = body.into_inner(); 28 | 29 | let dest = generate_uuid(); 30 | let task_id = generate_uuid(); 31 | 32 | let src_url = get_download_url(&body.source); 33 | let dst_url = get_upload_url(&dest); 34 | 35 | let new_job = NewJob { 36 | submitter: user.user_name, 37 | type_: String::from("build"), 38 | source: body.source, 39 | metadata: body.metadata, 40 | status: None, 41 | destination: Some(dest.clone()), 42 | task_id: Some(task_id.clone()), 43 | }; 44 | let job_id = conn 45 | .transaction::<_, diesel::result::Error, _>(|| { 46 | diesel::insert_into(jobs::table) 47 | .values(&new_job) 48 | .execute(&conn)?; 49 | let job_id = jobs::dsl::jobs 50 | .select(jobs::dsl::id) 51 | .filter(jobs::dsl::task_id.eq(&task_id)) 52 | .first::(&conn)?; 53 | Ok(job_id) 54 | }) 55 | .map_err(err)?; 56 | 57 | get_task_manager().do_send(SubmitBuildTask { 58 | id: task_id.clone(), 59 | src: src_url, 60 | dst: dst_url, 61 | timestamp: get_timestamp(), 62 | }); 63 | return Ok(HttpResponse::Ok().json(job_id)); 64 | } 65 | Ok(HttpResponse::Forbidden().finish()) 66 | } 67 | 68 | #[derive(Serialize, Deserialize)] 69 | struct FinishRequest { 70 | task_id: String, 71 | status: String, 72 | } 73 | 74 | #[post("/finish")] 75 | async fn finish(body: web::Json, pool: web::Data) -> Result { 76 | let conn = pool.get().map_err(err)?; 77 | if let Ok(mut job) = jobs::dsl::jobs 78 | .filter(jobs::dsl::task_id.eq(&body.task_id)) 79 | .first::(&conn) 80 | { 81 | if job.status.is_none() { 82 | // not finished 83 | job.status = Some(body.status.clone()); 84 | job.finished_at = Some(Utc::now()); 85 | return Ok( 86 | HttpResponse::Ok().json(diesel::update(&job).set(&job).execute(&conn).is_ok()) 87 | ); 88 | } 89 | return Ok(HttpResponse::Ok().json(true)); 90 | } 91 | Ok(HttpResponse::Forbidden().finish()) 92 | } 93 | 94 | #[derive(Serialize, Deserialize)] 95 | struct JobListRequest { 96 | offset: Option, 97 | limit: Option, 98 | } 99 | 100 | #[derive(Serialize, Deserialize)] 101 | struct JobListResponse { 102 | offset: i64, 103 | limit: i64, 104 | jobs: Vec, 105 | } 106 | 107 | #[derive(Serialize, Deserialize)] 108 | struct JobInfo { 109 | id: i32, 110 | submitter: String, 111 | type_: String, 112 | metadata: String, 113 | status: Option, 114 | src_url: String, 115 | dst_url: Option, 116 | created_at: DateTime, 117 | finished_at: Option>, 118 | } 119 | 120 | impl From for JobInfo { 121 | fn from(job: Job) -> JobInfo { 122 | let src_url = get_download_url(&job.source); 123 | let dst_url = job.destination.map(|dest| get_download_url(&dest)); 124 | JobInfo { 125 | id: job.id, 126 | submitter: job.submitter, 127 | type_: job.type_, 128 | metadata: job.metadata, 129 | status: job.status, 130 | src_url, 131 | dst_url, 132 | created_at: job.created_at, 133 | finished_at: job.finished_at, 134 | } 135 | } 136 | } 137 | 138 | #[get("/list")] 139 | async fn list( 140 | sess: Session, 141 | pool: web::Data, 142 | query: web::Query, 143 | ) -> Result { 144 | let conn = pool.get().map_err(err)?; 145 | if let (Some(user), conn) = get_user(&sess, conn).await? { 146 | if user.role == "admin" { 147 | let offset = query.offset.unwrap_or(0); 148 | let limit = query.limit.unwrap_or(5); 149 | let jobs = web::block(move || { 150 | let query = jobs::dsl::jobs.order(jobs::dsl::id.desc()).offset(offset); 151 | if limit >= 0 { 152 | query.limit(limit).load::(&conn) 153 | } else { 154 | query.load::(&conn) 155 | } 156 | }) 157 | .await 158 | .map_err(err)?; 159 | return Ok(HttpResponse::Ok().json(JobListResponse { 160 | offset, 161 | limit, 162 | jobs: jobs.into_iter().map(JobInfo::from).collect(), 163 | })); 164 | } 165 | } 166 | Ok(HttpResponse::Forbidden().finish()) 167 | } 168 | 169 | #[get("/count")] 170 | async fn count(sess: Session, pool: web::Data) -> Result { 171 | let conn = pool.get().map_err(err)?; 172 | if let (Some(user), conn) = get_user(&sess, conn).await? { 173 | if user.role == "admin" { 174 | let count = web::block(move || jobs::dsl::jobs.count().get_result::(&conn)) 175 | .await 176 | .map_err(err)?; 177 | return Ok(HttpResponse::Ok().json(count)); 178 | } 179 | } 180 | Ok(HttpResponse::Forbidden().finish()) 181 | } 182 | 183 | #[get("/")] 184 | async fn list_self( 185 | sess: Session, 186 | pool: web::Data, 187 | query: web::Query, 188 | ) -> Result { 189 | let conn = pool.get().map_err(err)?; 190 | if let (Some(user), conn) = get_user(&sess, conn).await? { 191 | let offset = query.offset.unwrap_or(0); 192 | let limit = query.limit.unwrap_or(5); 193 | 194 | let res = jobs::dsl::jobs 195 | .filter(jobs::dsl::submitter.eq(user.user_name)) 196 | .order(jobs::dsl::id.desc()) 197 | .offset(offset) 198 | .limit(limit) 199 | .load::(&conn); 200 | 201 | if let Ok(jobs) = res { 202 | return Ok(HttpResponse::Ok().json(JobListResponse { 203 | offset, 204 | limit, 205 | jobs: jobs.into_iter().map(JobInfo::from).collect(), 206 | })); 207 | } 208 | } 209 | Ok(HttpResponse::Forbidden().finish()) 210 | } 211 | 212 | #[get("/get/{job_id}")] 213 | async fn get(sess: Session, pool: web::Data, path: web::Path) -> Result { 214 | let conn = pool.get().map_err(err)?; 215 | if let (Some(user), conn) = get_user(&sess, conn).await? { 216 | if let Ok(job) = jobs::dsl::jobs.find(*path).first::(&conn) { 217 | if user.role == "admin" || user.user_name == job.submitter { 218 | return Ok(HttpResponse::Ok().json(JobInfo::from(job))); 219 | } 220 | } 221 | } 222 | Ok(HttpResponse::Forbidden().finish()) 223 | } 224 | -------------------------------------------------------------------------------- /backend/src/task_manager.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{generate_uuid, get_download_url, get_timestamp, get_upload_url}; 2 | use crate::env::ENV; 3 | use crate::models::*; 4 | use crate::schema::jobs; 5 | use crate::DbPool; 6 | use actix::prelude::*; 7 | use diesel::prelude::*; 8 | use log::*; 9 | use redis; 10 | use serde_derive::{Deserialize, Serialize}; 11 | use std::time::Duration; 12 | 13 | #[derive(Default)] 14 | pub struct TaskManagerActor { 15 | client: Option, 16 | conn: Option, 17 | db: Option, 18 | } 19 | 20 | impl actix::Supervised for TaskManagerActor {} 21 | 22 | impl Actor for TaskManagerActor { 23 | type Context = Context; 24 | 25 | fn started(&mut self, _ctx: &mut Context) {} 26 | } 27 | 28 | fn monitor( 29 | actor: &mut TaskManagerActor, 30 | ctx: &mut Context, 31 | ) -> Result<(), failure::Error> { 32 | if let Some(db) = &actor.db { 33 | if let Some(client) = &actor.client { 34 | let conn = if let Some(conn) = &mut actor.conn { 35 | conn 36 | } else { 37 | let conn = client.get_connection()?; 38 | actor.conn = Some(conn); 39 | actor.conn.as_mut().unwrap() // safe unwrap 40 | }; 41 | let db_conn = db.get()?; 42 | let len_waiting: u64 = redis::cmd("LLEN") 43 | .arg(&ENV.redis_waiting_queue) 44 | .query(conn)?; 45 | let len_working: u64 = redis::cmd("LLEN") 46 | .arg(&ENV.redis_working_queue) 47 | .query(conn)?; 48 | if len_waiting > 0 || len_working > 0 { 49 | info!( 50 | "task queue: {} waiting, {} working", 51 | len_waiting, len_working 52 | ); 53 | while let Some(last_working) = redis::cmd("LINDEX") 54 | .arg(&ENV.redis_working_queue) 55 | .arg("-1") 56 | .query::>(conn)? 57 | { 58 | if let Ok(task) = serde_json::from_str::(&last_working) { 59 | // find job by task 60 | if let Ok(mut job) = jobs::dsl::jobs 61 | .filter(jobs::dsl::task_id.eq(&task.id)) 62 | .first::(&db_conn) 63 | { 64 | if job.status.is_some() { 65 | // done, remove it 66 | redis::cmd("RPOP") 67 | .arg(&ENV.redis_working_queue) 68 | .query(conn)?; 69 | info!("task queue: removing finished task {}", task.id,); 70 | } else { 71 | if get_timestamp() - task.timestamp > 5 * 60 { 72 | // timeout, assign a new task id and destination 73 | let new_task_id = generate_uuid(); 74 | let new_dest = generate_uuid(); 75 | job.task_id = Some(new_task_id.clone()); 76 | job.destination = Some(new_dest.clone()); 77 | let src_url = get_download_url(&job.source); 78 | let dst_url = get_upload_url(&new_dest); 79 | diesel::update(&job).set(&job).execute(&db_conn)?; 80 | ctx.address().do_send(SubmitBuildTask { 81 | id: new_task_id.clone(), 82 | src: src_url, 83 | dst: dst_url, 84 | timestamp: get_timestamp(), 85 | }); 86 | redis::cmd("RPOP") 87 | .arg(&ENV.redis_working_queue) 88 | .query(conn)?; 89 | info!( 90 | "task queue: restarting task {} -> {}", 91 | task.id, new_task_id 92 | ); 93 | } else { 94 | // no timeout tasks 95 | break; 96 | } 97 | } 98 | } else { 99 | redis::cmd("RPOP") 100 | .arg(&ENV.redis_working_queue) 101 | .query(conn)?; 102 | info!("task queue: removing stale task {}", task.id,); 103 | } 104 | } else { 105 | // bad element, remove it 106 | redis::cmd("RPOP") 107 | .arg(&ENV.redis_working_queue) 108 | .query(conn)?; 109 | info!("task queue: removing unknown element from working queue"); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | Ok(()) 117 | } 118 | 119 | impl SystemService for TaskManagerActor { 120 | fn service_started(&mut self, ctx: &mut Context) { 121 | info!("task manager is up"); 122 | let client = redis::Client::open(ENV.redis_url.clone()).expect("redis client"); 123 | self.client = Some(client); 124 | ctx.run_interval(Duration::from_secs(10), |actor, ctx| { 125 | if let Err(err) = monitor(actor, ctx) { 126 | warn!("Error occurred in task manager: {}", err); 127 | sentry::capture_message( 128 | &format!("Error occurred in task manager : {}", err), 129 | sentry::Level::Error, 130 | ); 131 | // close connection and try again 132 | actor.conn = None; 133 | } 134 | }); 135 | } 136 | } 137 | 138 | #[derive(Message, Serialize, Deserialize)] 139 | #[rtype(result = "bool")] 140 | pub struct SubmitBuildTask { 141 | pub id: String, 142 | pub src: String, 143 | pub dst: String, 144 | pub timestamp: u64, 145 | } 146 | 147 | impl Handler for TaskManagerActor { 148 | type Result = bool; 149 | 150 | fn handle(&mut self, req: SubmitBuildTask, _ctx: &mut Context) -> bool { 151 | if let Some(conn) = self.conn.as_mut() { 152 | if redis::cmd("LPUSH") 153 | .arg(&ENV.redis_waiting_queue) 154 | .arg(serde_json::to_string(&req).expect("to json")) 155 | .query::<()>(conn) 156 | .is_ok() 157 | { 158 | return true; 159 | } 160 | } else { 161 | warn!("no redis conn, fail to submit build task"); 162 | } 163 | false 164 | } 165 | } 166 | 167 | #[derive(Message)] 168 | #[rtype(result = "()")] 169 | pub struct SetDb { 170 | pub db: DbPool, 171 | } 172 | 173 | impl Handler for TaskManagerActor { 174 | type Result = (); 175 | 176 | fn handle(&mut self, req: SetDb, _ctx: &mut Context) { 177 | self.db = Some(req.db); 178 | } 179 | } 180 | 181 | #[derive(Message)] 182 | #[rtype(result = "GetMetricResponse")] 183 | pub struct GetMetric; 184 | 185 | #[derive(MessageResponse)] 186 | pub struct GetMetricResponse { 187 | pub len_waiting: u64, 188 | pub len_working: u64, 189 | } 190 | 191 | impl Handler for TaskManagerActor { 192 | type Result = GetMetricResponse; 193 | 194 | fn handle(&mut self, _req: GetMetric, _ctx: &mut Context) -> GetMetricResponse { 195 | let mut len_waiting = 0; 196 | let mut len_working = 0; 197 | if let Some(conn) = self.conn.as_mut() { 198 | len_waiting = redis::cmd("LLEN") 199 | .arg(&ENV.redis_waiting_queue) 200 | .query(conn) 201 | .unwrap_or(0); 202 | len_working = redis::cmd("LLEN") 203 | .arg(&ENV.redis_working_queue) 204 | .query(conn) 205 | .unwrap_or(0); 206 | } 207 | GetMetricResponse { 208 | len_waiting, 209 | len_working, 210 | } 211 | } 212 | } 213 | 214 | pub fn get_task_manager() -> Addr { 215 | TaskManagerActor::from_registry() 216 | } 217 | -------------------------------------------------------------------------------- /examples/4m-clock-digital-life.json: -------------------------------------------------------------------------------- 1 | { 2 | "redux": { 3 | "top": "DigitalLife", 4 | "signals": { 5 | "display[6]": 4, 6 | "display[5]": 5, 7 | "display[4]": 6, 8 | "clk": 3, 9 | "display[3]": 7, 10 | "display[2]": 8, 11 | "display[1]": 9, 12 | "display[0]": 10, 13 | "rst": 0, 14 | "display2[3]": 15, 15 | "display1[3]": 11, 16 | "display2[2]": 16, 17 | "display1[2]": 12, 18 | "display2[1]": 17, 19 | "display1[1]": 13, 20 | "display2[0]": 18, 21 | "display1[0]": 14 22 | }, 23 | "code": "library ieee;\r\nuse ieee.std_logic_1164.all;\r\nuse ieee.std_logic_unsigned.all;\r\n\r\nentity DigitalLife is\r\n port (\r\n rst: in std_logic;\r\n clk: in std_logic;\r\n display: out std_logic_vector(6 downto 0);\r\n display1: out std_logic_vector(3 downto 0);\r\n display2: out std_logic_vector(3 downto 0)\r\n \r\n );\r\nend entity;\r\n\r\n\r\n\r\narchitecture main of DigitalLife is\r\n signal count: std_logic_vector(3 downto 0);\r\n signal count1: std_logic_vector(3 downto 0);\r\n signal count2: std_logic_vector(3 downto 0);\r\n signal timeCount: std_logic_vector(31 downto 0);\r\nbegin\r\n process (clk, rst) begin\r\n if (rst = '1') then\r\n count <= (others => '0');\r\n count1 <= (others => '0');\r\n count2 <= (0 => '1', others => '0');\r\n timeCount <= (others => '0');\r\n elsif (rising_edge(clk)) then\r\n if (timeCount = 4_000_000) then\r\n count <= count + 1;\r\n if (count1 = 8) then\r\n count1 <= (others => '0');\r\n else\r\n count1 <= count1 + 2;\r\n end if;\r\n if (count2 = 9) then\r\n count2 <= (0 =>'1', others => '0');\r\n else\r\n count2 <= count2 + 2;\r\n end if;\r\n timeCount <= (others => '0');\r\n else\r\n timeCount <= timeCount + 1;\r\n end if;\r\n end if;\r\n end process;\r\n\r\n process (count) begin\r\n case count is\r\n when \"0000\" =>\r\n display <= \"0111111\";\r\n when \"0001\" =>\r\n display <= \"0000110\";\r\n when \"0010\" =>\r\n display <= \"1011011\";\r\n when \"0011\" =>\r\n display <= \"1001111\";\r\n when \"0100\" =>\r\n display <= \"1100110\";\r\n when \"0101\" =>\r\n display <= \"1101101\";\r\n when \"0110\" =>\r\n display <= \"1111101\";\r\n when \"0111\" =>\r\n display <= \"0000111\";\r\n when \"1000\" =>\r\n display <= \"1111111\";\r\n when \"1001\" =>\r\n display <= \"1101111\";\r\n when \"1010\" =>\r\n display <= \"1110111\";\r\n when \"1011\" =>\r\n display <= \"1111100\";\r\n when \"1100\" =>\r\n display <= \"0111001\";\r\n when \"1101\" =>\r\n display <= \"1011110\";\r\n when \"1110\" =>\r\n display <= \"1111001\";\r\n when others =>\r\n display <= \"1110001\";\r\n end case;\r\n end process;\r\n \r\n display1 <= count1;\r\n display2 <= count2;\r\n\r\nend architecture main;\r\n", 24 | "field": [ 25 | { 26 | "type": "FPGA", 27 | "x": 0, 28 | "y": 0, 29 | "id": "fpga", 30 | "persistent": true 31 | }, 32 | { 33 | "type": "Switch4", 34 | "x": 0, 35 | "y": 175, 36 | "id": "switch4_1" 37 | }, 38 | { 39 | "type": "Digit4", 40 | "x": 175, 41 | "y": 0, 42 | "id": "digit4_1" 43 | }, 44 | { 45 | "type": "Digit7", 46 | "x": 350, 47 | "y": 0, 48 | "id": "digit7_1" 49 | }, 50 | { 51 | "type": "Clock", 52 | "x": 175, 53 | "y": 175, 54 | "id": "clock_1" 55 | } 56 | ] 57 | }, 58 | "sandbox": { 59 | "groups": { 60 | "dbc5ef43-a217-4ee8-a19f-08b2b48bc1ac": [ 61 | "clock_1-clk", 62 | "fpga-0" 63 | ], 64 | "ec5fc9da-b888-45f4-96ed-b871db074389": [ 65 | "fpga-3", 66 | "clock_1-4" 67 | ], 68 | "df034a06-db55-4274-aeb9-515361d0a076": [ 69 | "digit7_1-A-G", 70 | "fpga-4" 71 | ], 72 | "b18bf006-72e4-433d-a525-b55c0abdc450": [ 73 | "digit7_1-A-F", 74 | "fpga-5" 75 | ], 76 | "c731791b-de81-4198-b047-97f964ed6953": [ 77 | "fpga-6", 78 | "digit7_1-A-E" 79 | ], 80 | "db3d1645-e5a8-4b41-9d37-0337fbda2525": [ 81 | "digit7_1-A-D", 82 | "fpga-7" 83 | ], 84 | "c390f3db-7b66-4f5a-8065-7d73143fcc7e": [ 85 | "digit7_1-A-C", 86 | "fpga-8" 87 | ], 88 | "39b632d1-8de0-42f3-8a87-7aad50ec9bc4": [ 89 | "fpga-9", 90 | "digit7_1-A-B" 91 | ], 92 | "c77c75ab-c38a-4210-8a14-94fa97cb3654": [ 93 | "fpga-10", 94 | "digit7_1-A-A" 95 | ], 96 | "1f36ee01-5de6-4a30-9518-d488ccd971e3": [ 97 | "fpga-11", 98 | "digit4_1-A-8" 99 | ], 100 | "b8e18046-6d9d-4106-a26a-d1a1c352a995": [ 101 | "fpga-12", 102 | "digit4_1-A-4" 103 | ], 104 | "2740739d-0464-4e7f-8ab3-06d55192ac34": [ 105 | "fpga-13", 106 | "digit4_1-A-2" 107 | ], 108 | "f2ff0738-59f6-4f39-b880-5c02dc6265c7": [ 109 | "fpga-14", 110 | "digit4_1-A-1" 111 | ], 112 | "abc0441c-525d-4677-979d-d438cf704f4d": [ 113 | "fpga-15", 114 | "digit4_1-B-8" 115 | ], 116 | "41d594fc-ccff-4696-b569-0de39a07cb6b": [ 117 | "fpga-16", 118 | "digit4_1-B-4" 119 | ], 120 | "f56921cf-740d-4c7b-a574-5a10fc349168": [ 121 | "fpga-17", 122 | "digit4_1-B-2" 123 | ], 124 | "2cdc646f-c9c5-4785-a0ea-dc6c88cb51da": [ 125 | "fpga-18", 126 | "digit4_1-B-1" 127 | ] 128 | }, 129 | "colors": { 130 | "dbc5ef43-a217-4ee8-a19f-08b2b48bc1ac": "#000000", 131 | "ec5fc9da-b888-45f4-96ed-b871db074389": "#000000", 132 | "df034a06-db55-4274-aeb9-515361d0a076": "#000000", 133 | "b18bf006-72e4-433d-a525-b55c0abdc450": "#000000", 134 | "c731791b-de81-4198-b047-97f964ed6953": "#000000", 135 | "db3d1645-e5a8-4b41-9d37-0337fbda2525": "#000000", 136 | "c390f3db-7b66-4f5a-8065-7d73143fcc7e": "#000000", 137 | "39b632d1-8de0-42f3-8a87-7aad50ec9bc4": "#000000", 138 | "c77c75ab-c38a-4210-8a14-94fa97cb3654": "#000000", 139 | "1f36ee01-5de6-4a30-9518-d488ccd971e3": "#000000", 140 | "b8e18046-6d9d-4106-a26a-d1a1c352a995": "#000000", 141 | "2740739d-0464-4e7f-8ab3-06d55192ac34": "#000000", 142 | "f2ff0738-59f6-4f39-b880-5c02dc6265c7": "#000000", 143 | "abc0441c-525d-4677-979d-d438cf704f4d": "#000000", 144 | "41d594fc-ccff-4696-b569-0de39a07cb6b": "#000000", 145 | "f56921cf-740d-4c7b-a574-5a10fc349168": "#000000", 146 | "2cdc646f-c9c5-4785-a0ea-dc6c88cb51da": "#000000" 147 | }, 148 | "color": "#f44336" 149 | }, 150 | "lang": "vhdl" 151 | } -------------------------------------------------------------------------------- /examples/const-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "redux": { 3 | "top": "test", 4 | "signals": { 5 | "const0": 4, 6 | "const1": 5 7 | }, 8 | "code": "module test(\n\tconst0,\n\tconst1\n\t);\n\n\toutput wire const0;\n\toutput wire const1;\n\tassign const0 = 0;\n\tassign const1 = 1;\n\nendmodule", 9 | "field": [ 10 | { 11 | "type": "FPGA", 12 | "x": 0, 13 | "y": 0, 14 | "id": "fpga", 15 | "persistent": true 16 | }, 17 | { 18 | "type": "Switch4", 19 | "x": 0, 20 | "y": 175, 21 | "id": "switch4_1" 22 | }, 23 | { 24 | "type": "Digit4", 25 | "x": 175, 26 | "y": 0, 27 | "id": "digit4_1" 28 | }, 29 | { 30 | "type": "Digit7", 31 | "x": 350, 32 | "y": 0, 33 | "id": "digit7_1" 34 | }, 35 | { 36 | "type": "Clock", 37 | "x": 175, 38 | "y": 175, 39 | "id": "clock_1" 40 | } 41 | ] 42 | }, 43 | "sandbox": { 44 | "groups": { 45 | "eb06d95c-cd5e-4dbd-9a45-2d9a938343db": [ 46 | "fpga-4", 47 | "digit4_1-A-1" 48 | ], 49 | "58bc06bd-be76-4a26-ba82-91d05a4b78b4": [ 50 | "fpga-5", 51 | "digit4_1-B-1" 52 | ] 53 | }, 54 | "colors": { 55 | "eb06d95c-cd5e-4dbd-9a45-2d9a938343db": "#000000", 56 | "58bc06bd-be76-4a26-ba82-91d05a4b78b4": "#000000" 57 | }, 58 | "color": "#000000" 59 | }, 60 | "lang": "verilog" 61 | } 62 | -------------------------------------------------------------------------------- /examples/digital_life.vhdl: -------------------------------------------------------------------------------- 1 | library ieee; 2 | use ieee.std_logic_1164.all; 3 | use ieee.std_logic_unsigned.all; 4 | 5 | entity mod_top is 6 | port ( 7 | clk :in std_logic; 8 | rst :in std_logic; 9 | digital :out std_logic_vector (6 downto 0); 10 | odd :out std_logic_vector (3 downto 0); 11 | even :out std_logic_vector (3 downto 0) 12 | ); 13 | end entity; 14 | architecture rtl of mod_top is 15 | signal counter :std_logic_vector (31 downto 0) := (others => '0'); 16 | signal count :std_logic_vector (3 downto 0) := (others => '0'); 17 | signal odd_reg :std_logic_vector (3 downto 0) := (others => '0'); 18 | signal even_reg :std_logic_vector (3 downto 0) := (others => '0'); 19 | 20 | component decoder is 21 | port ( 22 | number :in std_logic_vector (3 downto 0); 23 | digital :out std_logic_vector (6 downto 0) 24 | ); 25 | end component; 26 | begin 27 | decoder_inst : decoder port map (count, digital); 28 | process (clk, rst) begin 29 | if (rst = '1') then 30 | count <= (others => '0'); 31 | odd_reg <= (0 => '1', others => '0'); 32 | even_reg <= (others => '0'); 33 | counter <= (others => '0'); 34 | elsif (rising_edge(clk)) then 35 | if (counter <= 1_048_576) then 36 | counter <= counter + 1; 37 | else 38 | counter <= (others => '0'); 39 | if (count <= 8) then 40 | count <= count + 1; 41 | else 42 | count <= (others => '0'); 43 | end if; 44 | 45 | if (odd_reg <= 8) then 46 | odd_reg <= odd_reg + 2; 47 | else 48 | odd_reg <= (0 => '1', others => '0'); 49 | end if; 50 | if (even_reg <= 6) then 51 | even_reg <= even_reg + 2; 52 | else 53 | even_reg <= (others => '0'); 54 | end if; 55 | end if; 56 | end if; 57 | end process; 58 | 59 | odd <= odd_reg; 60 | even <= even_reg; 61 | end architecture; 62 | 63 | library ieee; 64 | use ieee.std_logic_1164.all; 65 | use ieee.std_logic_unsigned.all; 66 | 67 | entity decoder is 68 | port ( 69 | number :in std_logic_vector (3 downto 0); 70 | digital :out std_logic_vector (6 downto 0) 71 | ); 72 | end entity; 73 | architecture rtl of decoder is 74 | begin 75 | process (number) begin 76 | case number is 77 | when "0000" => 78 | digital <= "0111111"; 79 | when "0001" => 80 | digital <= "0000110"; 81 | when "0010" => 82 | digital <= "1011011"; 83 | when "0011" => 84 | digital <= "1001111"; 85 | when "0100" => 86 | digital <= "1100110"; 87 | when "0101" => 88 | digital <= "1101101"; 89 | when "0110" => 90 | digital <= "1111101"; 91 | when "0111" => 92 | digital <= "0000111"; 93 | when "1000" => 94 | digital <= "1111111"; 95 | when "1001" => 96 | digital <= "1101111"; 97 | when others => 98 | digital <= "0000000"; 99 | end case; 100 | end process; 101 | end architecture; -------------------------------------------------------------------------------- /examples/manual-clock-digital-life.json: -------------------------------------------------------------------------------- 1 | { 2 | "redux": { 3 | "top": "series", 4 | "signals": { 5 | "display[6]": 5, 6 | "display[5]": 6, 7 | "display[4]": 7, 8 | "clk": 3, 9 | "display[3]": 8, 10 | "display[2]": 9, 11 | "display[1]": 10, 12 | "display[0]": 11, 13 | "rst": 0, 14 | "display_4_2[3]": 16, 15 | "display_4_1[3]": 12, 16 | "display_4_2[2]": 17, 17 | "display_4_1[2]": 13, 18 | "display_4_2[1]": 18, 19 | "display_4_1[1]": 14, 20 | "display_4_2[0]": 19, 21 | "display_4_1[0]": 15 22 | }, 23 | "code": "library ieee;\nuse ieee.std_logic_1164.all;\nuse ieee.std_logic_arith.all;\nuse ieee.std_logic_unsigned.all;\n\nentity series is\n\tport(\n\t\tclk:in std_logic;\n\t\trst:in std_logic;\n\t\tdisplay:out std_logic_vector(6 downto 0); \n\t\tdisplay_4_1:out std_logic_vector(3 downto 0); \n\t\tdisplay_4_2:out std_logic_vector(3 downto 0) \n\t\t);\nend series;\n\narchitecture bhv of series is\n\nsignal key1:std_logic_vector(3 downto 0);\nsignal key2:std_logic_vector(3 downto 0);\nsignal key3:std_logic_vector(3 downto 0);\n\nbegin\n\tdisplay_4_1<=key2;\n\tdisplay_4_2<=key3;\n\t\n\tprocess(clk,rst)\n\tbegin\n\t\tif(rst = '0') then\n\t\t\tkey1<=\"0000\";\n\t\t\tkey2<=\"0001\";\n\t\t\tkey3<=\"0000\";\n\t\telsif (clk'event and clk= '1') then \n\t\t\n\t\t\tif(key1=\"1001\") then\n\t\t\t\tkey1<=\"0000\";\n\t\t\telse\n\t\t\t\tkey1<=key1+1;\n\t\t\tend if;\n\n\t\t\tif(key2=\"1001\") then\n\t\t\t\tkey2<=\"0001\";\n\t\t\telse\n\t\t\t\tkey2<=key2+2;\n\t\t\tend if;\n\n\t\t\tif(key3=\"1000\") then\n\t\t\t\tkey3<=\"0000\";\n\t\t\telse\n\t\t\t\tkey3<=key3+2;\n\t\t\tend if;\n\t\tend if;\n\tend process;\n\t\n\tprocess(key1)\n\tbegin\n\t\tcase key1 is\n\t\t\twhen \"0000\"=>display<=\"1111110\"; \n\t\t\twhen \"0001\"=>display<=\"0110000\";\n\t\t\twhen \"0010\"=>display<=\"1101101\";\n\t\t\twhen \"0011\"=>display<=\"1111001\";\n\t\t\twhen \"0100\"=>display<=\"0110011\";\n\t\t\twhen \"0101\"=>display<=\"1011011\";\n\t\t\twhen \"0110\"=>display<=\"1011111\";\n\t\t\twhen \"0111\"=>display<=\"1110000\";\n\t\t\twhen \"1000\"=>display<=\"1111111\"; \n\t\t\twhen \"1001\"=>display<=\"1110011\"; \n\t\t\twhen others=>display<=\"0000000\";\n\t\tend case;\n\tend process;\nend bhv;\t\t\n", 24 | "field": [ 25 | { 26 | "type": "FPGA", 27 | "x": 0, 28 | "y": 0, 29 | "id": "fpga", 30 | "persistent": true 31 | }, 32 | { 33 | "type": "Switch4", 34 | "x": 0, 35 | "y": 175, 36 | "id": "switch4_1" 37 | }, 38 | { 39 | "type": "Digit4", 40 | "x": 175, 41 | "y": 0, 42 | "id": "digit4_1" 43 | }, 44 | { 45 | "type": "Digit7", 46 | "x": 350, 47 | "y": 0, 48 | "id": "digit7_1" 49 | }, 50 | { 51 | "type": "Clock", 52 | "x": 175, 53 | "y": 175, 54 | "id": "clock_1" 55 | } 56 | ] 57 | }, 58 | "sandbox": { 59 | "groups": { 60 | "e60dbb9e-dcf7-4a9a-8f85-8d1dc3d30e21": [ 61 | "fpga-0", 62 | "switch4_1-switch-3" 63 | ], 64 | "27c9b77c-b0cb-4494-9555-99d8ceddf9cd": [ 65 | "fpga-12", 66 | "digit4_1-A-8" 67 | ], 68 | "60431eb1-0b38-4bf3-b86e-30f9334fdeb2": [ 69 | "fpga-13", 70 | "digit4_1-A-4" 71 | ], 72 | "1cd0df16-4596-40a0-a315-ca303e270863": [ 73 | "fpga-14", 74 | "digit4_1-A-2" 75 | ], 76 | "ede0e7b2-430e-454b-9491-c312cb8b4867": [ 77 | "fpga-15", 78 | "digit4_1-A-1" 79 | ], 80 | "fdb308d1-5020-40aa-a042-ca7a41fb9ccd": [ 81 | "fpga-16", 82 | "digit4_1-B-8" 83 | ], 84 | "12f67449-6a35-46ec-b65c-5d9d11ecf68d": [ 85 | "fpga-17", 86 | "digit4_1-B-4" 87 | ], 88 | "0179f1ea-8e02-44f4-b0e3-26c3b929e307": [ 89 | "fpga-18", 90 | "digit4_1-B-2" 91 | ], 92 | "be7b6dcc-9d16-4106-8fd3-ac2de7a05a68": [ 93 | "fpga-19", 94 | "digit4_1-B-1" 95 | ], 96 | "92cab5ad-ebf7-4877-9ae3-de481c01c07a": [ 97 | "fpga-5", 98 | "digit7_1-A-A" 99 | ], 100 | "45d97325-8715-4d0a-a239-3536e1c4b039": [ 101 | "digit7_1-A-B", 102 | "fpga-6" 103 | ], 104 | "b04c4fb5-983a-4b36-af70-58e9997dfdd2": [ 105 | "fpga-7", 106 | "digit7_1-A-C" 107 | ], 108 | "7e45964b-1244-4e44-a467-070555fb5e91": [ 109 | "fpga-8", 110 | "digit7_1-A-D" 111 | ], 112 | "f89269e7-cee5-4b50-8881-4799734cac76": [ 113 | "fpga-9", 114 | "digit7_1-A-E" 115 | ], 116 | "b7102a85-6180-4379-b635-99326941956b": [ 117 | "fpga-10", 118 | "digit7_1-A-F" 119 | ], 120 | "c65fa3f7-d30e-4275-a9f8-fe63a92bf242": [ 121 | "fpga-11", 122 | "digit7_1-A-G" 123 | ], 124 | "cb432218-6d7f-4fdf-a2ef-ca5ecc0709d5": [ 125 | "clock_1-clk", 126 | "fpga-3" 127 | ] 128 | }, 129 | "colors": { 130 | "e60dbb9e-dcf7-4a9a-8f85-8d1dc3d30e21": "#000000", 131 | "27c9b77c-b0cb-4494-9555-99d8ceddf9cd": "#000000", 132 | "60431eb1-0b38-4bf3-b86e-30f9334fdeb2": "#000000", 133 | "1cd0df16-4596-40a0-a315-ca303e270863": "#000000", 134 | "ede0e7b2-430e-454b-9491-c312cb8b4867": "#000000", 135 | "fdb308d1-5020-40aa-a042-ca7a41fb9ccd": "#000000", 136 | "12f67449-6a35-46ec-b65c-5d9d11ecf68d": "#000000", 137 | "0179f1ea-8e02-44f4-b0e3-26c3b929e307": "#000000", 138 | "be7b6dcc-9d16-4106-8fd3-ac2de7a05a68": "#000000", 139 | "92cab5ad-ebf7-4877-9ae3-de481c01c07a": "#000000", 140 | "45d97325-8715-4d0a-a239-3536e1c4b039": "#000000", 141 | "b04c4fb5-983a-4b36-af70-58e9997dfdd2": "#000000", 142 | "7e45964b-1244-4e44-a467-070555fb5e91": "#000000", 143 | "f89269e7-cee5-4b50-8881-4799734cac76": "#000000", 144 | "b7102a85-6180-4379-b635-99326941956b": "#000000", 145 | "c65fa3f7-d30e-4275-a9f8-fe63a92bf242": "#000000", 146 | "cb432218-6d7f-4fdf-a2ef-ca5ecc0709d5": "#000000" 147 | }, 148 | "color": "#000000" 149 | }, 150 | "lang": "vhdl" 151 | } 152 | -------------------------------------------------------------------------------- /examples/multi-pin-net-manual-clock.json: -------------------------------------------------------------------------------- 1 | { 2 | "redux": { 3 | "top": "{\"EXP_IO[3]\":0,\"EXP_IO[2]\":4,\"EXP_IO[1]\":5,\"EXP_IO[0]\":6}", 4 | "signals": { 5 | "display[6]": 5, 6 | "display[5]": 6, 7 | "display[4]": 7, 8 | "clk": 3, 9 | "display[3]": 8, 10 | "display[2]": 9, 11 | "display[1]": 10, 12 | "display[0]": 11, 13 | "rst": 0, 14 | "display_4_2[3]": 16, 15 | "display_4_1[3]": 12, 16 | "display_4_2[2]": 17, 17 | "display_4_1[2]": 13, 18 | "display_4_2[1]": 18, 19 | "display_4_1[1]": 14, 20 | "display_4_2[0]": 19, 21 | "display_4_1[0]": 15 22 | }, 23 | "code": "library ieee;\n use ieee.std_logic_1164.all;\n use ieee.std_logic_unsigned.all;\n\nentity mod_top is\n port (\n M_nRESET :in std_logic;\n CLK_100M :in std_logic;\n EXP_IO :out std_logic_vector (3 downto 0)\n );\nend entity;\narchitecture rtl of mod_top is\n signal output :std_logic_vector (3 downto 0) := (0 => '1', others => '0');\n signal counter :std_logic_vector (31 downto 0) := (others => '0');\nbegin\n process (CLK_100M, M_nRESET) begin\n if (M_nRESET = '0') then\n output <= (0 => '1', others => '0');\n counter <= (others => '0');\n elsif (rising_edge(CLK_100M)) then\n if (counter = 1_000_000) then\n counter <= (others => '0');\n if (output(3) = '1') then\n output <= (0 => '1', others => '0');\n else\n output <= output(2 downto 0) & '0';\n end if;\n else\n counter <= counter + 1;\n end if;\n end if;\n end process;\n EXP_IO <= output;\nend architecture;\n\n", 24 | "field": [ 25 | { 26 | "type": "FPGA", 27 | "x": 0, 28 | "y": 0, 29 | "id": "fpga", 30 | "persistent": true 31 | }, 32 | { 33 | "type": "Switch4", 34 | "x": 0, 35 | "y": 175, 36 | "id": "switch4_1" 37 | }, 38 | { 39 | "type": "Digit4", 40 | "x": 175, 41 | "y": 0, 42 | "id": "digit4_1" 43 | }, 44 | { 45 | "type": "Digit7", 46 | "x": 350, 47 | "y": 0, 48 | "id": "digit7_1" 49 | }, 50 | { 51 | "type": "Clock", 52 | "x": 175, 53 | "y": 175, 54 | "id": "clock_1" 55 | } 56 | ] 57 | }, 58 | "sandbox": { 59 | "groups": { 60 | "0ad48749-caf0-49ab-a9b2-eba1430ed302": [ 61 | "switch4_1-switch-3", 62 | "fpga-3", 63 | "switch4_1-switch-2" 64 | ] 65 | }, 66 | "colors": { 67 | "0ad48749-caf0-49ab-a9b2-eba1430ed302": "#000000" 68 | }, 69 | "color": "#000000" 70 | }, 71 | "lang": "vhdl" 72 | } -------------------------------------------------------------------------------- /examples/running-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "redux": { 3 | "top": "mod_top", 4 | "signals": { 5 | "M_nRESET": 0, 6 | "CLK_100M": 3, 7 | "EXP_IO[3]": 4, 8 | "EXP_IO[2]": 5, 9 | "EXP_IO[1]": 6, 10 | "EXP_IO[0]": 7 11 | }, 12 | "code": "library ieee;\n use ieee.std_logic_1164.all;\n use ieee.std_logic_unsigned.all;\n\nentity mod_top is\n port (\n M_nRESET :in std_logic;\n CLK_100M :in std_logic;\n EXP_IO :out std_logic_vector (3 downto 0)\n );\nend entity;\narchitecture rtl of mod_top is\n signal output :std_logic_vector (3 downto 0) := (0 => '1', others => '0');\n signal counter :std_logic_vector (31 downto 0) := (others => '0');\nbegin\n process (CLK_100M, M_nRESET) begin\n if (M_nRESET = '0') then\n output <= (0 => '1', others => '0');\n counter <= (others => '0');\n elsif (rising_edge(CLK_100M)) then\n if (counter = 1_000_000) then\n counter <= (others => '0');\n if (output(3) = '1') then\n output <= (0 => '1', others => '0');\n else\n output <= output(2 downto 0) & '0';\n end if;\n else\n counter <= counter + 1;\n end if;\n end if;\n end process;\n EXP_IO <= output;\nend architecture;\n\n", 13 | "field": [ 14 | { 15 | "type": "FPGA", 16 | "x": 0, 17 | "y": 0, 18 | "id": "fpga", 19 | "persistent": true 20 | }, 21 | { 22 | "type": "Switch4", 23 | "x": 0, 24 | "y": 175, 25 | "id": "switch4_1" 26 | }, 27 | { 28 | "type": "Digit4", 29 | "x": 175, 30 | "y": 0, 31 | "id": "digit4_1" 32 | }, 33 | { 34 | "type": "Digit7", 35 | "x": 350, 36 | "y": 0, 37 | "id": "digit7_1" 38 | }, 39 | { 40 | "type": "Clock", 41 | "x": 175, 42 | "y": 175, 43 | "id": "clock_1" 44 | } 45 | ] 46 | }, 47 | "sandbox": { 48 | "groups": { 49 | "759cafa7-bab3-4e0f-8396-90c4868bd391": [ 50 | "fpga-3", 51 | "clock_1-1" 52 | ], 53 | "933404e8-e4ec-4b89-8258-571efe153d1f": [ 54 | "fpga-0", 55 | "clock_1-clk" 56 | ], 57 | "6a013009-7c5e-4a01-93f5-70479c46ce2b": [ 58 | "fpga-4", 59 | "switch4_1-led-3" 60 | ], 61 | "22621c4f-0aa3-42af-9d48-67ae10aebaaf": [ 62 | "fpga-5", 63 | "switch4_1-led-2" 64 | ], 65 | "af51e295-5c62-4655-8e97-8d3a01cd16d8": [ 66 | "fpga-6", 67 | "switch4_1-led-1" 68 | ], 69 | "c0cebd2a-9ce6-4106-9c09-c76b304856f4": [ 70 | "fpga-7", 71 | "switch4_1-led-0" 72 | ] 73 | }, 74 | "colors": { 75 | "759cafa7-bab3-4e0f-8396-90c4868bd391": "#f44336", 76 | "933404e8-e4ec-4b89-8258-571efe153d1f": "#f44336", 77 | "6a013009-7c5e-4a01-93f5-70479c46ce2b": "#f44336", 78 | "22621c4f-0aa3-42af-9d48-67ae10aebaaf": "#f44336", 79 | "af51e295-5c62-4655-8e97-8d3a01cd16d8": "#f44336", 80 | "c0cebd2a-9ce6-4106-9c09-c76b304856f4": "#f44336" 81 | }, 82 | "color": "#f44336" 83 | }, 84 | "lang": "vhdl" 85 | } -------------------------------------------------------------------------------- /examples/running_light.vhdl: -------------------------------------------------------------------------------- 1 | library ieee; 2 | use ieee.std_logic_1164.all; 3 | use ieee.std_logic_unsigned.all; 4 | 5 | entity mod_top is 6 | port ( 7 | M_nRESET :in std_logic; 8 | CLK_100M :in std_logic; 9 | EXP_IO :out std_logic_vector (3 downto 0) 10 | ); 11 | end entity; 12 | architecture rtl of mod_top is 13 | signal output :std_logic_vector (3 downto 0) := (0 => '1', others => '0'); 14 | signal counter :std_logic_vector (31 downto 0) := (others => '0'); 15 | begin 16 | process (CLK_100M, M_nRESET) begin 17 | if (M_nRESET = '0') then 18 | output <= (0 => '1', others => '0'); 19 | counter <= (others => '0'); 20 | elsif (rising_edge(CLK_100M)) then 21 | if (counter = 1_000_000) then 22 | counter <= (others => '0'); 23 | if (output(3) = '1') then 24 | output <= (0 => '1', others => '0'); 25 | else 26 | output <= output(2 downto 0) & '0'; 27 | end if; 28 | else 29 | counter <= counter + 1; 30 | end if; 31 | end if; 32 | end process; 33 | EXP_IO <= output; 34 | end architecture; 35 | 36 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "ignorePatterns": ["src/lib/pkg/**", "node_modules/**"], 4 | "rules": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Config 26 | src/config.js 27 | src/config-*.js 28 | 29 | .*.sw[a-p] 30 | 31 | .sentryclirc 32 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.61.0-slim AS wasm 2 | 3 | WORKDIR /wasm 4 | COPY ./src/lib . 5 | 6 | RUN sed -i -e 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' -e 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list 7 | RUN apt update 8 | RUN apt install -y curl 9 | 10 | RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 11 | RUN wasm-pack build 12 | 13 | FROM node:18 AS builder 14 | 15 | WORKDIR /app 16 | 17 | # RUN yarn global add @sentry/cli 18 | RUN curl -L https://github.com/getsentry/sentry-cli/releases/download/1.63.1/sentry-cli-Linux-x86_64 -o /usr/local/bin/sentry-cli 19 | RUN chmod +x /usr/local/bin/sentry-cli 20 | 21 | ADD package.json . 22 | ADD yarn.lock . 23 | RUN yarn install --frozen-lockfile 24 | COPY --from=wasm /wasm/pkg /app/src/lib/pkg 25 | 26 | ADD . . 27 | 28 | RUN ln -s config.example.js src/config.js 29 | ARG backend 30 | ARG sentry 31 | ARG base 32 | 33 | ARG commit_sha 34 | RUN PUBLIC_URL=$base CI_COMMIT_SHA=$commit_sha REACT_APP_BACKEND=$backend REACT_APP_SENTRY=$sentry yarn build 35 | 36 | ARG sentry_config 37 | RUN echo $sentry_config | base64 -d > .sentryclirc 38 | RUN sentry-cli releases new $commit_sha 39 | RUN sentry-cli releases files $commit_sha upload-sourcemaps /app/build --url-prefix ~$base --validate 40 | RUN sentry-cli releases finalize $commit_sha 41 | 42 | FROM nginx:1.18-alpine 43 | ENV TZ=Asia/Shanghai 44 | COPY nginx.conf /etc/nginx/conf.d/default.conf 45 | COPY mime.types /etc/nginx/mime.types 46 | COPY --from=builder /app/build /usr/share/nginx/html/jie 47 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # JieLabs Web 2 | 3 | ## How to build 4 | 5 | Prerequisites includes: 6 | 7 | - yarn 8 | - rust & cargo 9 | - wasm-pack 10 | 11 | To build the backend, simply issues an `cargo build` in the backend directory. 12 | 13 | To build the frontend: 14 | 15 | ```bash 16 | 17 | cd frontend/lib 18 | wasm-pack build 19 | cd pkg 20 | yarn link 21 | 22 | cd ../.. 23 | yarn link jielabs_lib 24 | 25 | yarn build 26 | # Or yarn watch 27 | ``` 28 | 29 | You may need to rerun `wasm-pack build` when the wasm code is updated. 30 | -------------------------------------------------------------------------------- /frontend/assets/logo.studio: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/assets/logo.studio -------------------------------------------------------------------------------- /frontend/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | docker pull $REGISTRY/thu-cs-lab/jielabs-frontend:wasm || true 5 | docker build --target wasm --cache-from $REGISTRY/thu-cs-lab/jielabs-frontend:wasm $DOCKER_ARGS -t thu-cs-lab/jielabs-frontend:wasm . 6 | docker tag thu-cs-lab/jielabs-frontend:wasm $REGISTRY/thu-cs-lab/jielabs-frontend:wasm 7 | docker push $REGISTRY/thu-cs-lab/jielabs-frontend:wasm 8 | 9 | docker pull $REGISTRY/thu-cs-lab/jielabs-frontend:builder || true 10 | docker build --target builder \ 11 | --cache-from $REGISTRY/thu-cs-lab/jielabs-frontend:wasm \ 12 | --cache-from $REGISTRY/thu-cs-lab/jielabs-frontend:builder \ 13 | --build-arg commit_sha=$CI_COMMIT_SHA \ 14 | --build-arg sentry=$SENTRY \ 15 | --build-arg backend=$BACKEND \ 16 | --build-arg base=$BASE \ 17 | --build-arg sentry_config=$SENTRY_CONFIG \ 18 | $DOCKER_ARGS -t thu-cs-lab/jielabs-frontend:builder . 19 | docker tag thu-cs-lab/jielabs-frontend:builder $REGISTRY/thu-cs-lab/jielabs-frontend:builder 20 | docker push $REGISTRY/thu-cs-lab/jielabs-frontend:builder 21 | 22 | docker pull $REGISTRY/thu-cs-lab/jielabs-frontend:latest || true 23 | docker build \ 24 | --cache-from $REGISTRY/thu-cs-lab/jielabs-frontend:wasm \ 25 | --cache-from $REGISTRY/thu-cs-lab/jielabs-frontend:builder \ 26 | --cache-from $REGISTRY/thu-cs-lab/jielabs-frontend:latest \ 27 | --build-arg commit_sha=$CI_COMMIT_SHA \ 28 | --build-arg sentry=$SENTRY \ 29 | --build-arg backend=$BACKEND \ 30 | --build-arg base=$BASE \ 31 | --build-arg sentry_config=$SENTRY_CONFIG \ 32 | $DOCKER_ARGS -t thu-cs-lab/jielabs-frontend:latest . 33 | docker tag thu-cs-lab/jielabs-frontend:latest $REGISTRY/thu-cs-lab/jielabs-frontend:latest 34 | docker push $REGISTRY/thu-cs-lab/jielabs-frontend:latest 35 | -------------------------------------------------------------------------------- /frontend/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { 2 | override, 3 | addWebpackPlugin, 4 | addWebpackModuleRule, 5 | adjustWorkbox, 6 | useEslintRc, 7 | setWebpackOptimizationSplitChunks, 8 | } = require('customize-cra'); 9 | 10 | const MonacoPlugin = require('monaco-editor-webpack-plugin'); 11 | const AnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 12 | const { GenerateSW } = require('workbox-webpack-plugin'); 13 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 14 | 15 | const path = require('path'); 16 | const webpack = require('webpack'); 17 | 18 | let commitInfo = process.env.CI_COMMIT_SHA; 19 | if(!commitInfo) 20 | commitInfo = require('child_process') 21 | .execSync('git describe --all --dirty --long') 22 | .toString(); 23 | 24 | const allOverrides = [ 25 | addWebpackPlugin(new MonacoPlugin({ 26 | languages: [], 27 | })), 28 | addWebpackPlugin(new webpack.DefinePlugin({ 29 | __COMMIT_HASH__: JSON.stringify(commitInfo), 30 | })), 31 | addWebpackPlugin(new AnalyzerPlugin({ analyzerMode: (!!process.env.ANALYZE) ? 'static' : 'none', openAnalyzer: false })), 32 | addWebpackPlugin(new WasmPackPlugin({ 33 | crateDirectory: path.resolve(__dirname, './src/lib'), 34 | outDir: path.resolve(__dirname, './src/lib/pkg'), 35 | })), 36 | addWebpackModuleRule({ test: /\.wasm$/, type: 'webassembly/async' }), 37 | addWebpackModuleRule({ test: /\.vhdl/, use: 'raw-loader' }), 38 | addWebpackModuleRule({ test: /\.v/, use: 'raw-loader' }), 39 | // useEslintRc(), 40 | ]; 41 | 42 | const prodOverrides = [ 43 | addWebpackPlugin( 44 | new GenerateSW({ 45 | navigateFallback: `${process.env.PUBLIC_URL || ''}/index.html`, 46 | navigateFallbackDenylist: [ 47 | /\/api\/.*/, // TODO: properly does this based on BACKEND in config 48 | new RegExp('/[^/?]+\\.[^/]+$'), 49 | ], 50 | maximumFileSizeToCacheInBytes: 8388608, 51 | }) 52 | ), 53 | setWebpackOptimizationSplitChunks({ 54 | chunks: 'all', 55 | name: false, 56 | 57 | cacheGroups: { 58 | monaco: { 59 | test: /[\\/]node_modules[\\/]monaco-editor/, 60 | reuseExistingChunk: false, 61 | }, 62 | }, 63 | }) 64 | ]; 65 | 66 | const devOverrides = []; 67 | 68 | function createOverrider(env) { 69 | if(env === 'development') return override(...allOverrides, ...devOverrides); 70 | else return override(...allOverrides, ...prodOverrides); 71 | } 72 | 73 | module.exports = function(config, env) { 74 | const overrider = createOverrider(env); 75 | const overriden = overrider(config, env); 76 | overriden.experiments = { 77 | ...overriden.experiments, 78 | asyncWebAssembly: true, 79 | }; 80 | overriden.infrastructureLogging = { 81 | debug: true, 82 | level: 'verbose', 83 | }; 84 | console.log(overriden); 85 | return overriden; 86 | } 87 | 88 | -------------------------------------------------------------------------------- /frontend/mime.types: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/javascript js; 9 | application/wasm wasm; 10 | application/atom+xml atom; 11 | application/rss+xml rss; 12 | 13 | text/mathml mml; 14 | text/plain txt; 15 | text/vnd.sun.j2me.app-descriptor jad; 16 | text/vnd.wap.wml wml; 17 | text/x-component htc; 18 | 19 | image/png png; 20 | image/svg+xml svg svgz; 21 | image/tiff tif tiff; 22 | image/vnd.wap.wbmp wbmp; 23 | image/webp webp; 24 | image/x-icon ico; 25 | image/x-jng jng; 26 | image/x-ms-bmp bmp; 27 | 28 | font/woff woff; 29 | font/woff2 woff2; 30 | 31 | application/java-archive jar war ear; 32 | application/json json; 33 | application/mac-binhex40 hqx; 34 | application/msword doc; 35 | application/pdf pdf; 36 | application/postscript ps eps ai; 37 | application/rtf rtf; 38 | application/vnd.apple.mpegurl m3u8; 39 | application/vnd.google-earth.kml+xml kml; 40 | application/vnd.google-earth.kmz kmz; 41 | application/vnd.ms-excel xls; 42 | application/vnd.ms-fontobject eot; 43 | application/vnd.ms-powerpoint ppt; 44 | application/vnd.oasis.opendocument.graphics odg; 45 | application/vnd.oasis.opendocument.presentation odp; 46 | application/vnd.oasis.opendocument.spreadsheet ods; 47 | application/vnd.oasis.opendocument.text odt; 48 | application/vnd.openxmlformats-officedocument.presentationml.presentation 49 | pptx; 50 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 51 | xlsx; 52 | application/vnd.openxmlformats-officedocument.wordprocessingml.document 53 | docx; 54 | application/vnd.wap.wmlc wmlc; 55 | application/x-7z-compressed 7z; 56 | application/x-cocoa cco; 57 | application/x-java-archive-diff jardiff; 58 | application/x-java-jnlp-file jnlp; 59 | application/x-makeself run; 60 | application/x-perl pl pm; 61 | application/x-pilot prc pdb; 62 | application/x-rar-compressed rar; 63 | application/x-redhat-package-manager rpm; 64 | application/x-sea sea; 65 | application/x-shockwave-flash swf; 66 | application/x-stuffit sit; 67 | application/x-tcl tcl tk; 68 | application/x-x509-ca-cert der pem crt; 69 | application/x-xpinstall xpi; 70 | application/xhtml+xml xhtml; 71 | application/xspf+xml xspf; 72 | application/zip zip; 73 | 74 | application/octet-stream bin exe dll; 75 | application/octet-stream deb; 76 | application/octet-stream dmg; 77 | application/octet-stream iso img; 78 | application/octet-stream msi msp msm; 79 | 80 | audio/midi mid midi kar; 81 | audio/mpeg mp3; 82 | audio/ogg ogg; 83 | audio/x-m4a m4a; 84 | audio/x-realaudio ra; 85 | 86 | video/3gpp 3gpp 3gp; 87 | video/mp2t ts; 88 | video/mp4 mp4; 89 | video/mpeg mpeg mpg; 90 | video/quicktime mov; 91 | video/webm webm; 92 | video/x-flv flv; 93 | video/x-m4v m4v; 94 | video/x-mng mng; 95 | video/x-ms-asf asx asf; 96 | video/x-ms-wmv wmv; 97 | video/x-msvideo avi; 98 | } -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | location / { 5 | root /usr/share/nginx/html; 6 | try_files $uri $uri/ /jie/index.html; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@juggle/resize-observer": "^3.3.0", 7 | "@sentry/react": "^6.2.3", 8 | "@sentry/tracing": "^6.2.3", 9 | "classnames": "^2.2.6", 10 | "file-saver": "^2.0.5", 11 | "immutable": "^4.0.0-rc.12", 12 | "monaco-editor": "^0.33.0", 13 | "pako": "^2.0.3", 14 | "react": "^18.1.0", 15 | "react-dom": "^18.1.0", 16 | "react-monaco-editor": "^0.48.0", 17 | "react-redux": "^8.0.2", 18 | "react-router-dom": "^6.3.0", 19 | "react-transition-group": "^4.3.0", 20 | "redux": "^4.0.5", 21 | "redux-logger": "^3.0.6", 22 | "redux-thunk": "^2.3.0", 23 | "util": "^0.12.4", 24 | "uuid": "^8.3.2" 25 | }, 26 | "scripts": { 27 | "start": "react-app-rewired start", 28 | "build": "react-app-rewired build", 29 | "test": "react-app-rewired test", 30 | "eject": "react-scripts eject", 31 | "lint": "eslint src" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@wasm-tool/wasm-pack-plugin": "^1.6.0", 47 | "customize-cra": "^1.0.0", 48 | "monaco-editor-webpack-plugin": "^7.0.1", 49 | "raw-loader": "^4.0.2", 50 | "react-app-rewired": "^2.1.8", 51 | "react-scripts": "^5.0.1", 52 | "sass": "^1.52.1", 53 | "webpack-bundle-analyzer": "^4.5.0", 54 | "workbox-webpack-plugin": "^6.5.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | JieLabs 25 | 26 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
Calling jiegec
37 |
Tuning k8s
38 |
Starting actix-web
39 |
Rendering React
40 |
Compiling Rust
41 |
Heating GPU with WebKit
42 |
Downloading Quartus
43 |
Parsing WebAssembly
44 |
Patting meow
45 |
Connecting to Redis
46 |
47 |
48 |
49 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /frontend/public/logo/logo-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/public/logo/logo-flat.png -------------------------------------------------------------------------------- /frontend/public/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/public/logo/logo.png -------------------------------------------------------------------------------- /frontend/public/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/public/logo/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/public/logo/logo@2x.png -------------------------------------------------------------------------------- /frontend/public/logo/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/public/logo/logo@3x.png -------------------------------------------------------------------------------- /frontend/public/logo/logo@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/public/logo/logo@4x.png -------------------------------------------------------------------------------- /frontend/public/logo/logo@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/public/logo/logo@8x.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "JieLabs", 3 | "name": "JieLabs online experiments", 4 | "icons": [ 5 | { 6 | "src": "logo/logo.png", 7 | "sizes": "64x64", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "logo/logo@2x.png", 12 | "sizes": "128x128", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "logo/logo@3x.png", 17 | "sizes": "192x192", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "logo/logo@4x.png", 22 | "sizes": "256x256", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "logo/logo@8x.png", 27 | "sizes": "512x512", 28 | "type": "image/png" 29 | } 30 | ], 31 | "start_url": ".", 32 | "display": "standalone", 33 | "theme_color": "#4068e0", 34 | "background_color": "#333" 35 | } 36 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import * as Sentry from '@sentry/browser'; 3 | 4 | import Icon from './comps/Icon'; 5 | 6 | export default class ErrorBoundary extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { error: null, evid: null }; 10 | } 11 | 12 | static getDerivedStateFromError(error) { 13 | return { error }; 14 | } 15 | 16 | componentDidCatch(error, errorInfo) { 17 | Sentry.withScope(scope => { 18 | scope.setExtras(errorInfo); 19 | const evid = Sentry.captureException(error); 20 | this.setState({ evid }); 21 | }); 22 | } 23 | 24 | refresh() { 25 | window.location.reload(); 26 | } 27 | 28 | forceRefresh() { 29 | window.localStorage.clear(); 30 | window.location.reload(true); 31 | } 32 | 33 | render() { 34 | const { children } = this.props; 35 | if(!this.state.error) return children; 36 | 37 | return ( 38 |
39 |
JieLabs 预■体验成员内■版本遇到问题
40 |
杰哥正在寻找该问题的解决方案...
41 |
42 | 43 |
44 | 我们已经收集了错误的基本信息,请尝试刷新能否解决这个问题。如果问题依旧存在,您可以尝试点击下面的按钮清空本地存储并刷新。 45 |
46 | 47 |
48 | 49 | 50 |
51 | 52 |
{ this.state.evid || '上传中...' }
53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/assets/tutorial.v: -------------------------------------------------------------------------------- 1 | module top ( 2 | rst, 3 | clk 4 | toggle 5 | ); 6 | 7 | input wire rst; 8 | input wire clk; 9 | output wire toggle; 10 | 11 | reg out; 12 | reg [31:0] counter; 13 | 14 | assign toggle = out; 15 | 16 | always @ (posedge clk) begin 17 | if (rst) begin 18 | out <= 1; 19 | counter <= 0; 20 | end else begin 21 | if (counter == 32'd4_000_000) begin 22 | counter <= 0; 23 | out <= ~out; 24 | end else begin 25 | counter <= counter + 1; 26 | end 27 | end 28 | end 29 | 30 | endmodule -------------------------------------------------------------------------------- /frontend/src/assets/tutorial.vhdl: -------------------------------------------------------------------------------- 1 | library ieee; 2 | use ieee.std_logic_1164.all; 3 | use ieee.std_logic_unsigned.all; 4 | 5 | entity top is 6 | port ( 7 | rst: in std_logic; 8 | clk: in std_logic; 9 | toggle: out std_logic; 10 | ); 11 | end entity; 12 | architecture rtl of top is 13 | signal output :std_logic := '1'; 14 | signal counter :std_logic_vector (31 downto 0) := (others => '0'); 15 | begin 16 | process (clk, rst) begin 17 | if (rst = '1') then 18 | output <= '1'; 19 | counter <= (others => '0'); 20 | elsif (rising_edge(clk)) then 21 | if (counter = 4_000_000) then 22 | counter <= (others => '0'); 23 | output <= not output; 24 | else 25 | counter <= counter + 1 26 | end if; 27 | end if; 28 | end process; 29 | toggle <= output; 30 | end architecture meow; 31 | -------------------------------------------------------------------------------- /frontend/src/blocks/Clock.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import { Connector, SIGNAL, MODE } from './index.js'; 5 | 6 | import { TimeoutContext } from '../App'; 7 | 8 | const CLOCK_FREQUENCY = [16, 8, 4, 2, 1]; 9 | 10 | export default React.memo(({ id, ...rest }) => { 11 | const [manualClock, setManualClock] = useState(SIGNAL.L); 12 | const [manualReset, setManualReset] = useState(SIGNAL.L); 13 | 14 | const timeoutCtx = useContext(TimeoutContext); 15 | 16 | function getHandler(setter, signal) { 17 | return e => { 18 | e.stopPropagation(); 19 | setter(signal); 20 | timeoutCtx.reset(); 21 | } 22 | } 23 | 24 | return
25 |
26 |
27 | { 28 | CLOCK_FREQUENCY.map((f, idx) => 29 |
30 | 35 |
{f}M
36 |
) 37 | } 38 |
39 |
40 |
41 |
42 | 43 |
48 |
49 |
50 | 51 |
56 |
57 |
58 |
59 | }); 60 | -------------------------------------------------------------------------------- /frontend/src/blocks/Digit4.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useRef } from 'react'; 2 | import { List } from 'immutable'; 3 | 4 | import { Connector, SIGNAL } from './index.js'; 5 | 6 | import Digit from '../comps/Digit'; 7 | 8 | function toInteger(pins) { 9 | let result = 0; 10 | pins.forEach(pin => { 11 | result *= 2; 12 | if(pin === SIGNAL.H) result += 1; 13 | }); 14 | 15 | return result 16 | } 17 | 18 | const LABELS = ['8', '4', '2', '1']; 19 | 20 | export default React.memo(({ id, ...rest }) => { 21 | const DIGIT_LUT = useMemo(() => [ 22 | '1111110', // 0 23 | '0110000', // 1 24 | '1101101', // 2 25 | '1111001', // 3 26 | '0110011', // 4 27 | '1011011', // 5 28 | '1011111', // 6 29 | '1110000', // 7 30 | '1111111', // 8 31 | '1111011', // 9 32 | '0000000', // A 33 | '0000000', // B 34 | '0000000', // C 35 | '0000000', // D 36 | '0000000', // E 37 | '0000000', // F 38 | ].map( 39 | e => List(e.split('').map(e => e === '1' ? SIGNAL.H : SIGNAL.L)) 40 | ), []); 41 | 42 | function useGroup(name) { 43 | const [pins, setPins] = useState(List(new Array(4).fill(SIGNAL.L))); 44 | const curPins = useRef(pins); 45 | 46 | const integer = toInteger(pins); 47 | const sig = DIGIT_LUT[integer]; 48 | 49 | const elem = ( 50 |
51 |
52 | { pins.map((pin, idx) => ( 53 |
54 |
{ LABELS[idx] }
55 | { 58 | curPins.current = curPins.current.set(idx, val); 59 | setPins(curPins.current); 60 | }} 61 | /> 62 |
63 | ))} 64 |
65 |
66 | 67 |
68 |
69 | ); 70 | 71 | return [pins, elem]; 72 | } 73 | 74 | const groupA = useGroup('A'); 75 | const groupB = useGroup('B'); 76 | const groupC = useGroup('C'); 77 | 78 | return
79 | { groupA } 80 | { groupB } 81 | { groupC } 82 |
83 | }); 84 | -------------------------------------------------------------------------------- /frontend/src/blocks/Digit7.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { List } from 'immutable'; 3 | 4 | import { Connector, SIGNAL } from './index.js'; 5 | 6 | import Digit from '../comps/Digit'; 7 | 8 | const LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 9 | 10 | export default React.memo(({ id, ...rest }) => { 11 | function useGroup(name) { 12 | const [pins, setPins] = useState(List(new Array(7).fill(SIGNAL.L))); 13 | const curPins = useRef(pins); 14 | 15 | const elem = ( 16 |
17 |
18 | { pins.map((pin, idx) => ( 19 |
20 |
{ LABELS[idx] }
21 | { 24 | curPins.current = curPins.current.set(idx, val); 25 | setPins(curPins.current); 26 | }} 27 | /> 28 |
29 | ))} 30 |
31 |
32 | 33 |
34 |
35 | ); 36 | 37 | return [pins, elem]; 38 | } 39 | 40 | const groupA = useGroup('A'); 41 | const groupB = useGroup('B'); 42 | const groupC = useGroup('C'); 43 | 44 | return
45 | { groupA } 46 | { groupB } 47 | { groupC } 48 |
49 | }); 50 | -------------------------------------------------------------------------------- /frontend/src/blocks/FPGA.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useCallback, useContext } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 4 | 5 | import { Connector, SIGNAL, MODE } from './index.js'; 6 | import { FPGAEnvContext } from '../Sandbox'; 7 | import { TimeoutContext } from '../App'; 8 | import Icon from '../comps/Icon'; 9 | import { BOARDS } from '../config'; 10 | 11 | import { BOARD_STATUS, setOutput, connectToBoard } from '../store/actions'; 12 | 13 | export default React.memo(({ id, ...rest }) => { 14 | const input = useSelector(state => state.input); 15 | const directions = useSelector(state => state.activeBuild?.directions); 16 | const dispatch = useDispatch(); 17 | 18 | const status = useSelector(state => state.board.status); 19 | const ident = useSelector(state => state.board.ident); 20 | const boardTmpl = useSelector(state => BOARDS[state.constraints.board]); 21 | const boardTmplPins = boardTmpl.pins; 22 | 23 | const maxIdx = Math.max(...boardTmplPins.map((e, idx) => Number.isInteger(e.idx) ? e.idx : idx)); 24 | 25 | const paddedInput = useMemo(() => { 26 | // Build reverse direction mapping 27 | const revDir = {}; 28 | if(directions) 29 | for(const i in directions) { 30 | const ridx = boardTmplPins[i].idx ?? i; 31 | revDir[ridx] = directions[i]; // We are using objects here because typeof i === 'string' 32 | } 33 | 34 | const head = (input || []).map((e, idx) => { 35 | if(revDir[idx] === 'output') // Inputs from FPGA 36 | return SIGNAL.X; 37 | else 38 | return e; 39 | }); 40 | 41 | // TODO: slice based on board tmpl pin count 42 | if(head.length > maxIdx) return head; 43 | const tail = Array(maxIdx + 1 - head.length).fill(SIGNAL.X); 44 | 45 | return head.concat(tail); 46 | }, [input, directions, boardTmplPins, maxIdx]); 47 | 48 | const gridStyle = useMemo(() => { 49 | const slotCount = boardTmpl.dimensions[0] * boardTmpl.dimensions[1]; 50 | let paddingBottom = 0; 51 | if(slotCount === boardTmpl.pins.length && !boardTmpl.pins[slotCount-1].placeholder) 52 | paddingBottom = 10 + 15; 53 | 54 | return { 55 | paddingBottom, 56 | gridTemplateColumns: new Array(boardTmpl.dimensions[0]).fill('1fr').join(' '), 57 | gridTemplateRows: new Array(boardTmpl.dimensions[1]).fill('1fr').join(' '), 58 | }; 59 | }, [boardTmpl]); 60 | 61 | const timeoutCtx = useContext(TimeoutContext); 62 | const doConnect = useCallback(async (e) => { 63 | let target = null; 64 | if(e.altKey && e.shiftKey) 65 | target = prompt('Please input the target board ID'); 66 | 67 | const succ = await dispatch(connectToBoard(target)); 68 | 69 | if(succ) 70 | timeoutCtx.start(); 71 | }, [timeoutCtx, dispatch]); 72 | 73 | const ctx = useContext(FPGAEnvContext); 74 | 75 | function formatIdent(ident) { 76 | const segs = ident.split('.'); 77 | // const prev = segs.slice(0, segs.length-1).join('.') + '.'; 78 | const last = segs[segs.length-1]; 79 | return <> 80 | { last } 81 | 82 | } 83 | 84 | return
85 | { boardTmplPins.map((pin, idx) => { 86 | if(pin.placeholder) return
; 87 | const ridx = Number.isInteger(pin.idx) ? pin.idx : idx; 88 | 89 | return ( 90 |
91 | { 96 | if(directions && idx in directions && directions[idx] === 'output') 97 | dispatch(setOutput(ridx, updated)) 98 | }} 99 | onReg={pin.clock ? ctx.regClocking : null} 100 | onUnreg={pin.clock ? ctx.unregClocking : null} 101 | output={paddedInput[ridx]} 102 | /> 103 |
{ pin.label || ridx }
104 |
105 | ); 106 | })} 107 | 108 | 109 | { ident && ( 110 | 115 |
{ formatIdent(ident) }
116 |
117 | )} 118 |
119 | 120 | 121 | { status === BOARD_STATUS.DISCONNECTED && ( 122 | 126 |
127 |
128 | FPGA disconnected 129 |
130 | 131 | settings_ethernet 132 | 133 |
134 | click to connect 135 |
136 |
137 |
138 | )} 139 | 140 | { status === BOARD_STATUS.PROGRAMMING && ( 141 | 145 |
146 |
147 |
148 | 149 | )} 150 | 151 |
152 | }); 153 | -------------------------------------------------------------------------------- /frontend/src/blocks/Switch4.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useContext } from 'react'; 2 | import cn from 'classnames'; 3 | import { List } from 'immutable'; 4 | 5 | import { Connector, SIGNAL } from './index.js'; 6 | 7 | import { TimeoutContext } from '../App'; 8 | 9 | export default React.memo(({ id, ...rest }) => { 10 | const [leds, setLeds] = useState(List(Array(4).fill(SIGNAL.X))); 11 | const [switches, setSwitches] = useState(List(Array(4).fill(SIGNAL.L))); 12 | 13 | const timeoutCtx = useContext(TimeoutContext); 14 | 15 | const currentLeds = useRef(leds); 16 | 17 | return
18 |
19 | { 20 | leds.map((s, idx) =>
21 |
22 | { 25 | currentLeds.current = currentLeds.current.set(idx, v); 26 | setLeds(currentLeds.current); 27 | }} 28 | > 29 |
) 30 | } 31 |
32 |
33 | { 34 | switches.map((s, idx) =>
35 |
{ 38 | setSwitches(switches.set(idx, s === SIGNAL.L ? SIGNAL.H : SIGNAL.L)); 39 | timeoutCtx.reset(); 40 | }} 41 | onMouseDown={e => e.stopPropagation()} 42 | >
43 | 44 |
) 45 | } 46 |
47 |
48 | }); 49 | -------------------------------------------------------------------------------- /frontend/src/blocks/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState, useContext, useEffect } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import { SandboxContext } from '../Sandbox.js'; 5 | 6 | export const SIGNAL = Object.freeze({ 7 | H: Symbol('High'), 8 | L: Symbol('Low'), 9 | X: Symbol('X'), 10 | }); 11 | 12 | /** 13 | * Connector mode. 14 | * 15 | * Normal pins can be connected to normal pins or ClockDest pins 16 | * ClockSrc pins can only be connected to ClockDest pins 17 | * ClockDest pins can be connected to any pins 18 | */ 19 | export const MODE = Object.freeze({ 20 | NORMAL: Symbol('Normal'), 21 | CLOCK_SRC: Symbol('ClockSrc'), 22 | CLOCK_DEST: Symbol('ClockDest'), 23 | }); 24 | 25 | /** 26 | * Note: currently, `mode`, `onReg` and `onUnreg` are not reactive 27 | */ 28 | export const Connector = React.memo(({ 29 | output: _output, 30 | mode: _mode, 31 | id: _id, 32 | data, 33 | onChange, 34 | onReg, 35 | onUnreg, 36 | className, 37 | ...rest 38 | }) => { 39 | const output = _output || SIGNAL.X; 40 | const mode = _mode || MODE.NORMAL; 41 | 42 | const snd = useContext(SandboxContext); 43 | 44 | const [id, setId] = useState(null); 45 | const [ref, setRef] = useState(null); 46 | 47 | const cb = useRef(onChange); 48 | const dataref = useRef(data); 49 | 50 | useEffect(() => { 51 | cb.current = onChange; 52 | }, [onChange]); 53 | 54 | useEffect(() => { 55 | dataref.current = data; 56 | }, [data]); 57 | 58 | useEffect(() => { 59 | const { id: nid, ref: nref } = snd.register(_id, cb, mode, dataref); 60 | setId(nid); 61 | setRef(nref); 62 | 63 | if(onReg) 64 | onReg(nid); 65 | 66 | return () => { 67 | snd.unregister(nid); 68 | if(onUnreg) 69 | onUnreg(); 70 | } 71 | }, [mode, onReg, onUnreg, snd, _id]); // TODO: do we need _id here? 72 | 73 | useEffect(() => { 74 | if(id !== null) 75 | snd.update(id, output); 76 | }, [id, output, snd]); 77 | 78 | const onClick = useCallback(() => { 79 | if(id !== null) 80 | snd.click(id); 81 | }, [id, snd]); 82 | 83 | if(!id) return
; 84 | return
e.stopPropagation()} 93 | >
; 94 | }); 95 | 96 | export { default as Switch4 } from './Switch4.js'; 97 | export { default as FPGA } from './FPGA.js'; 98 | export { default as Digit4 } from './Digit4.js'; 99 | export { default as Digit7 } from './Digit7.js'; 100 | export { default as Clock } from './Clock.js'; 101 | -------------------------------------------------------------------------------- /frontend/src/comps/Dialog.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 5 | 6 | export default React.memo(({ open, onClose, className, children, render, blocker, ...rest }) => { 7 | const weakBlocker = useCallback(e => e.stopPropagation(), []); 8 | 9 | useEffect(() => { 10 | if(!open) return; 11 | 12 | const listener = e => { 13 | if(e.key === 'Escape') 14 | onClose(); 15 | } 16 | 17 | document.addEventListener('keydown', listener); 18 | return () => document.removeEventListener('keydown', listener); 19 | }, [open, onClose]); 20 | 21 | if(!open) return ; 22 | 23 | const inner = render ? render() : children; 24 | 25 | return ( 26 | 27 | 31 |
32 |
33 | { inner } 34 |
35 |
36 |
37 |
38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /frontend/src/comps/Digit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import { SIGNAL } from '../blocks'; 5 | 6 | export default React.memo(({ values, className, ...rest }) => { 7 | const segs = []; 8 | 9 | for(let i = 0; i< 7; ++i) 10 | segs.push(
); 11 | 12 | return ( 13 |
14 |
15 | { segs } 16 |
17 |
18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/src/comps/Highlighter.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | function getClass(row, type) { 5 | if(type === 'constraints') { 6 | const isComment = row.match(/^\s*#/); 7 | if(isComment) return 'highlighter-comment'; 8 | else return null; 9 | } else { 10 | if(row.match(/^\s*Error/)) return 'highlighter-error'; 11 | if(row.match(/^\s*Warning/)) return 'highlighter-warning'; 12 | return null; 13 | } 14 | } 15 | 16 | export default React.memo(({ className, source, type }) => { 17 | const rows = useMemo(() => { 18 | const lines = source.split('\n'); 19 | 20 | return lines.map((line, idx) => ({ 21 | line: idx === lines.length - 1 ? line : line + '\n', 22 | class: getClass(line, type), 23 | })); 24 | }, [source, type]); 25 | 26 | return
27 |     { rows.map((e, idx) => {e.line}) }
28 |   
29 | }); 30 | -------------------------------------------------------------------------------- /frontend/src/comps/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | 4 | export default React.memo(({ children, className, ...rest }) => { children }); 5 | -------------------------------------------------------------------------------- /frontend/src/comps/Input.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | export default React.memo(({ onChange, label, value, placeholder, className, style, ...rest }) => { 5 | const cb = useCallback(ev => onChange(ev.target.value), [onChange]); 6 | 7 | return
8 |
{ label }
9 | 10 |
; 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/comps/Tooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | 4 | export default React.memo(({ tooltip, children, className, gap, ...rest }) => { 5 | return
6 |
7 | { children } 8 |
9 |
{ tooltip }
10 |
11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/config.example.js: -------------------------------------------------------------------------------- 1 | export const BACKEND = process.env.REACT_APP_BACKEND || 'http://localhost:8080'; 2 | export const WS_BACKEND = BACKEND.replace(/^http/, 'ws'); 3 | export const HARD_LOGOUT = false; 4 | export const CODE_ANALYSE_DEBOUNCE = 100; 5 | export const BUILD_POLL_INTERVAL = 3000; 6 | export const BUILD_LIST_FETCH_LENGTH = 5; 7 | 8 | export const SENTRY = process.env.REACT_APP_SENTRY || null; 9 | 10 | export const TAR_FILENAMES = { 11 | bitstream: 'bitstream.rbf', 12 | stdout: 'stdout', 13 | stderr: 'stderr', 14 | 15 | source: { 16 | 'vhdl': 'src/mod_top.vhd', 17 | 'verilog': 'src/mod_top.sv', 18 | }, 19 | constraints: 'src/mod_top.qsf', 20 | }; 21 | 22 | // From material.io color tool 23 | export const COLORS = [ 24 | '#000000', // BLACK 25 | '#f44336', // RED 500 26 | '#2196f3', // BLUE 500 27 | '#00acc1', // CYAN 600 28 | '#388e3c', // GREEN 700 29 | '#ffeb3b', // YELLOW 500 30 | '#f57c00', // ORANGE 700 31 | '#5d4037', // BROWN 700 32 | '#37474f', // BLUE GREY 800 33 | ]; 34 | 35 | export const DEFAULT_BOARD = 'default'; 36 | 37 | export const BOARDS = { 38 | default: { 39 | name: '数电实验', 40 | dimensions: [5, 4], 41 | pins: [ 42 | { pin: 'PIN_K24', input: true, output: true, clock: false, label: 'RST' }, 43 | { placeholder: true }, 44 | { placeholder: true }, 45 | { pin: 'PIN_R25', input: false, output: true, clock: true, label: 'CLK', idx: 37 }, 46 | { pin: 'PIN_L23', input: true, output: true, clock: false, label: 'IO21' }, 47 | { pin: 'PIN_M22', input: true, output: true, clock: false, label: 'IO3' }, 48 | { pin: 'PIN_J25', input: true, output: true, clock: false, label: 'IO6' }, 49 | { pin: 'PIN_J26', input: true, output: true, clock: false, label: 'IO14' }, 50 | { pin: 'PIN_K25', input: true, output: true, clock: false, label: 'IO17' }, 51 | { pin: 'PIN_M24', input: true, output: true, clock: false, label: 'IO20' }, 52 | { pin: 'PIN_M23', input: true, output: true, clock: false, label: 'IO2' }, 53 | { pin: 'PIN_K26', input: true, output: true, clock: false, label: 'IO5' }, 54 | { pin: 'PIN_L25', input: true, output: true, clock: false, label: 'IO8' }, 55 | { pin: 'PIN_N24', input: true, output: true, clock: false, label: 'IO16' }, 56 | { pin: 'PIN_N23', input: true, output: true, clock: false, label: 'IO19' }, 57 | { pin: 'PIN_M25', input: true, output: true, clock: false, label: 'IO1' }, 58 | { pin: 'PIN_AC24', input: true, output: true, clock: false, label: 'IO4' }, 59 | { pin: 'PIN_Y22', input: true, output: true, clock: false, label: 'IO7' }, 60 | { pin: 'PIN_AB24', input: true, output: true, clock: false, label: 'IO15' }, 61 | { pin: 'PIN_AB23', input: true, output: true, clock: false, label: 'IO18' }, 62 | ], 63 | }, 64 | }; 65 | 66 | export const BLOCK_ALIGNMENT = 175; 67 | 68 | export const DEFAULT_FIELD = [ 69 | { type: 'FPGA', x: 0, y: 0, id: 'fpga', persistent: true }, 70 | { type: 'Switch4', x: 0, y: 1 * BLOCK_ALIGNMENT, id: 'switch4_1' }, 71 | { type: 'Digit4', x: 1 * BLOCK_ALIGNMENT, y: 0, id: 'digit4_1' }, 72 | { type: 'Digit7', x: 2 * BLOCK_ALIGNMENT, y: 0, id: 'digit7_1' }, 73 | { type: 'Clock', x: 1 * BLOCK_ALIGNMENT, y: 1 * BLOCK_ALIGNMENT, id: 'clock_1' }, 74 | ]; 75 | 76 | export const TIMEOUT = 1000 * 60 * 30; 77 | export const TIMEOUT_BUFFER = 1000 * 60; 78 | -------------------------------------------------------------------------------- /frontend/src/fonts/MaterialIcons/MaterialIcons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/MaterialIcons/MaterialIcons.woff2 -------------------------------------------------------------------------------- /frontend/src/fonts/Roboto/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/Roboto/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /frontend/src/fonts/Roboto/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/Roboto/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /frontend/src/fonts/Roboto/Roboto-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/Roboto/Roboto-Thin.woff2 -------------------------------------------------------------------------------- /frontend/src/fonts/RobotoMono/RobotoMono-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/RobotoMono/RobotoMono-Bold.woff2 -------------------------------------------------------------------------------- /frontend/src/fonts/RobotoMono/RobotoMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/RobotoMono/RobotoMono-Regular.woff2 -------------------------------------------------------------------------------- /frontend/src/fonts/RobotoMono/RobotoMono-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/frontend/src/fonts/RobotoMono/RobotoMono-Thin.woff2 -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | 3 | import React from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | import * as Sentry from '@sentry/react'; 7 | import { Integrations } from "@sentry/tracing"; 8 | import { SENTRY } from './config'; 9 | 10 | import App from './App'; 11 | import * as serviceWorker from './serviceWorker'; 12 | 13 | import buildStore from './store'; 14 | import { showSnackbar } from './store/actions'; 15 | 16 | import ErrorBoundary from './ErrorBoundary'; 17 | 18 | import { Provider } from 'react-redux'; 19 | import { BrowserRouter } from 'react-router-dom'; 20 | 21 | /* Polyfills */ 22 | import { ResizeObserver, TextEncoder, TextDecoder } from './polyfills'; 23 | 24 | if(!window.ResizeObserver) 25 | window.ResizeObserver = ResizeObserver; 26 | 27 | if(!window.TextEncoder) 28 | window.TextEncoder = TextEncoder; 29 | 30 | if(!window.TextDecoder) 31 | window.TextDecoder = TextDecoder; 32 | 33 | /* Sentry */ 34 | if(SENTRY !== null) { 35 | const release = (__COMMIT_HASH__.match(/^heads\/(.*)[\n\r]+$/) ?? {})[1] ?? __COMMIT_HASH__; // eslint-disable-line no-undef 36 | Sentry.init({ 37 | dsn: SENTRY, 38 | integrations: [new Integrations.BrowserTracing()], 39 | release, 40 | }); 41 | } 42 | 43 | let storeSet = null; 44 | const storePromise = new Promise(resolve => { 45 | storeSet = resolve; 46 | }); 47 | 48 | const Root = React.memo(({ Comp }) => { 49 | const store = React.useMemo(() => buildStore(), []); 50 | 51 | React.useEffect(() => { 52 | import('./lang').then(mod => mod.default(store)); 53 | storeSet(store); 54 | }, [store]); 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }); 64 | 65 | const build = App => () => 66 | 67 | 68 | 69 | 70 | const Render = build(App); 71 | 72 | const root = createRoot(document.getElementById('root')); 73 | root.render(); 74 | 75 | // If you want your app to work offline and load faster, you can change 76 | // unregister() to register() below. Note this comes with some pitfalls. 77 | // Learn more about service workers: https://bit.ly/CRA-PWA 78 | serviceWorker.register({ 79 | async onUpdate(reg) { 80 | const store = await storePromise; 81 | store.dispatch(showSnackbar( 82 | 'Update available!', 83 | 0, 84 | () => { 85 | const waiting = reg.waiting; 86 | if(!waiting) return; 87 | waiting.postMessage({ type: 'SKIP_WAITING' }); 88 | setTimeout(() => window.location.reload(true), 50); // Wait for 50ms for the sw to activate 89 | }, 90 | 'REFRESH', 91 | )); 92 | } 93 | }); 94 | 95 | // Hot reloading 96 | if(module.hot) { 97 | module.hot.accept('./App', () => { 98 | const App = require('./App').default; 99 | const Render = build(App); 100 | root.render(); 101 | }); 102 | } 103 | 104 | /* eslint-disable no-undef */ 105 | console.log('Using version', __COMMIT_HASH__); 106 | -------------------------------------------------------------------------------- /frontend/src/lib/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | /target 3 | -------------------------------------------------------------------------------- /frontend/src/lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jielabs_lib" 3 | version = "0.1.0" 4 | authors = ["Liu Xiaoyi "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = { version = "0.2.80", features = ["serde-serialize"] } 12 | vhdl_lang = "0.17.0" 13 | serde = { version = "1.0.137", features = ["derive"] } 14 | maze-routing = { git = "https://github.com/jiegec/maze-routing" } 15 | verilog-lang = { git = "https://github.com/jiegec/verilog-lang" } 16 | 17 | [profile.release] 18 | lto = true 19 | opt-level = 's' 20 | -------------------------------------------------------------------------------- /frontend/src/lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | use maze_routing; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::Path; 4 | use wasm_bindgen::prelude::*; 5 | 6 | mod verilog; 7 | mod vhdl; 8 | 9 | #[cfg(not(test))] 10 | #[wasm_bindgen] 11 | extern "C" { 12 | #[wasm_bindgen(js_namespace=console)] 13 | fn log(s: &str); 14 | } 15 | 16 | #[cfg(test)] 17 | fn log(s: &str) { 18 | println!("{}", s); 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Debug)] 22 | struct Pos { 23 | from_line: u32, 24 | from_char: u32, 25 | to_line: u32, 26 | to_char: u32, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Debug, Clone)] 30 | #[serde(rename_all = "lowercase")] 31 | enum SignalDirection { 32 | Input, 33 | Output, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 37 | struct ArityInfo { 38 | from: u64, 39 | to: u64, 40 | } 41 | 42 | #[derive(Serialize, Deserialize, Debug)] 43 | struct SignalInfo { 44 | name: String, 45 | pos: Pos, 46 | dir: SignalDirection, 47 | arity: Option, 48 | } 49 | 50 | #[derive(Serialize, Deserialize, Debug)] 51 | struct EntityInfo { 52 | name: String, 53 | decl: Pos, 54 | pub(crate) signals: Vec, 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Debug)] 58 | #[serde(rename_all = "lowercase")] 59 | enum Severity { 60 | Hint, 61 | Info, 62 | Warning, 63 | Error, 64 | } 65 | 66 | #[derive(Serialize, Deserialize, Debug)] 67 | struct Diagnostic { 68 | pos: Pos, 69 | msg: String, 70 | severity: Severity, 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Default, Debug)] 74 | struct ParseResult { 75 | entities: Vec, 76 | top: Option, 77 | diagnostics: Vec, 78 | } 79 | 80 | #[wasm_bindgen] 81 | #[derive(Serialize, Deserialize)] 82 | pub enum Language { 83 | VHDL, 84 | Verilog, 85 | } 86 | 87 | // TODO: fallback top srcpos 88 | #[wasm_bindgen] 89 | pub fn parse(s: &str, top_name: Option, lang: Language) -> JsValue { 90 | let result = match lang { 91 | Language::VHDL => vhdl::parse(s, top_name), 92 | Language::Verilog => verilog::parse(s, top_name), 93 | }; 94 | 95 | JsValue::from_serde(&result).unwrap() 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/lib/src/vhdl.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use vhdl_lang::{Latin1String, Source, SrcPos, VHDLParser}; 3 | 4 | impl From for Pos { 5 | fn from(srcpos: SrcPos) -> Pos { 6 | Pos { 7 | from_line: srcpos.start().line, 8 | from_char: srcpos.start().character, 9 | 10 | to_line: srcpos.end().line, 11 | to_char: srcpos.end().character, 12 | } 13 | } 14 | } 15 | 16 | impl From for SignalDirection { 17 | fn from(mode: vhdl_lang::ast::Mode) -> SignalDirection { 18 | use vhdl_lang::ast::Mode; 19 | match mode { 20 | Mode::In => SignalDirection::Input, 21 | Mode::Out => SignalDirection::Output, 22 | _ => SignalDirection::Output, 23 | } 24 | } 25 | } 26 | 27 | impl From for Severity { 28 | fn from(s: vhdl_lang::Severity) -> Severity { 29 | use vhdl_lang::Severity::*; 30 | match s { 31 | Hint => Self::Hint, 32 | Info => Self::Info, 33 | Warning => Self::Warning, 34 | Error => Self::Error, 35 | } 36 | } 37 | } 38 | 39 | impl From for Diagnostic { 40 | fn from(d: vhdl_lang::Diagnostic) -> Diagnostic { 41 | Diagnostic { 42 | pos: d.pos.into(), 43 | msg: d.message, 44 | severity: d.severity.into(), 45 | } 46 | } 47 | } 48 | 49 | pub(crate) fn parse(s: &str, top_name: Option) -> ParseResult { 50 | let mut diagnostics = Vec::new(); 51 | let s = Source::inline(Path::new("design.vhdl"), s); 52 | let parser = VHDLParser::default(); 53 | let parsed = parser.parse_design_source(&s, &mut diagnostics); 54 | 55 | let mut top = None; 56 | let mut entities = Vec::new(); 57 | 58 | for unit in parsed.design_units { 59 | use vhdl_lang::ast::AnyDesignUnit::*; 60 | use vhdl_lang::ast::AnyPrimaryUnit::*; 61 | use vhdl_lang::ast::*; 62 | 63 | if let Primary(Entity(ent)) = unit { 64 | let name = ent.name().name(); 65 | 66 | let srcpos: &SrcPos = ent.ident().as_ref(); 67 | 68 | let mut entity = EntityInfo { 69 | name: name.to_string(), 70 | decl: srcpos.to_owned().into(), 71 | 72 | signals: Vec::new(), 73 | }; 74 | 75 | if let Some(ref ports) = ent.port_clause { 76 | for port in ports { 77 | if let InterfaceDeclaration::Object(decl) = port { 78 | // Parse arity 79 | let mut arity = None; 80 | 81 | if let Some(ref constraint) = decl.subtype_indication.constraint { 82 | let inner: &SubtypeConstraint = &constraint.item; 83 | //log(&format!("{:#?}", constraint)); 84 | 85 | if let &SubtypeConstraint::Array(ref vec, None) = inner { 86 | if let [DiscreteRange::Range(Range::Range(RangeConstraint { 87 | ref left_expr, 88 | ref right_expr, 89 | .. 90 | }))] = &vec[..] 91 | { 92 | if let Expression::Literal(Literal::AbstractLiteral( 93 | AbstractLiteral::Integer(left), 94 | )) = left_expr.item 95 | { 96 | if let Expression::Literal(Literal::AbstractLiteral( 97 | AbstractLiteral::Integer(right), 98 | )) = right_expr.item 99 | { 100 | arity = Some(ArityInfo { 101 | from: left, 102 | to: right, 103 | }); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | entity.signals.push(SignalInfo { 111 | name: decl.ident.to_string(), 112 | pos: decl.ident.as_ref().to_owned().into(), 113 | dir: decl.mode.into(), 114 | arity, 115 | }); 116 | } 117 | } 118 | } 119 | 120 | entities.push(entity); 121 | 122 | if let Some(ref top_ident) = top_name { 123 | let latin_ident = Latin1String::new(top_ident.as_bytes()); 124 | 125 | if name == &latin_ident { 126 | top = Some(entities.len() as u64 - 1); 127 | } 128 | } 129 | } 130 | } 131 | 132 | let result = ParseResult { 133 | entities, 134 | top, 135 | diagnostics: diagnostics.into_iter().map(Into::into).collect(), 136 | }; 137 | 138 | result 139 | } 140 | 141 | #[cfg(test)] 142 | mod test { 143 | use super::*; 144 | #[test] 145 | fn test_entity() { 146 | let result = parse( 147 | "entity mod_top is 148 | port ( 149 | in_1 :in std_logic; 150 | out_vec :out std_logic_vector (37 downto 0) 151 | ); 152 | end entity;", 153 | None, 154 | ); 155 | assert_eq!(result.entities.len(), 1); 156 | assert_eq!(result.entities[0].name, "mod_top"); 157 | assert_eq!(result.entities[0].signals.len(), 2); 158 | assert_eq!(result.entities[0].signals[0].name, "in_1"); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /frontend/src/loaders/Monaco.js: -------------------------------------------------------------------------------- 1 | import Monaco from 'react-monaco-editor'; 2 | export default Monaco; 3 | -------------------------------------------------------------------------------- /frontend/src/polyfills.js: -------------------------------------------------------------------------------- 1 | export { ResizeObserver } from '@juggle/resize-observer'; 2 | 3 | // From https://gist.github.com/joni/3760795/8f0c1a608b7f0c8b3978db68105c5b1d741d0446 4 | function toUTF8Array(str) { 5 | var utf8 = []; 6 | for (var i=0; i < str.length; i++) { 7 | var charcode = str.charCodeAt(i); 8 | if (charcode < 0x80) utf8.push(charcode); 9 | else if (charcode < 0x800) { 10 | utf8.push(0xc0 | (charcode >> 6), 11 | 0x80 | (charcode & 0x3f)); 12 | } 13 | else if (charcode < 0xd800 || charcode >= 0xe000) { 14 | utf8.push(0xe0 | (charcode >> 12), 15 | 0x80 | ((charcode>>6) & 0x3f), 16 | 0x80 | (charcode & 0x3f)); 17 | } 18 | // surrogate pair 19 | else { 20 | i++; 21 | charcode = ((charcode&0x3ff)<<10)|(str.charCodeAt(i)&0x3ff) 22 | utf8.push(0xf0 | (charcode >>18), 23 | 0x80 | ((charcode>>12) & 0x3f), 24 | 0x80 | ((charcode>>6) & 0x3f), 25 | 0x80 | (charcode & 0x3f)); 26 | } 27 | } 28 | return utf8; 29 | } 30 | 31 | 32 | export class TextEncoder { 33 | constructor(encoding) { 34 | console.log('Using custom encoder, expect some real slow motion, please upgrade your browser'); 35 | if(encoding && encoding.toLowerCase() !== 'utf-8' && encoding.toLowerCase() !== 'utf-16le') 36 | throw new Error(`Sorry, Meow's TextEncoder only supports UTF-8/16LE, required ${encoding}`); 37 | this.encoding = encoding.toLowerCase(); 38 | } 39 | 40 | encode(str) { 41 | if(this.encoding === 'utf-16le') { 42 | const buf = new ArrayBuffer(str.length * 2); 43 | const view = new Uint16Array(buf); 44 | 45 | for(let i = 0; i < str.length; ++i) 46 | view[i] = str.charCodeAt(i); 47 | 48 | return buf; 49 | } 50 | 51 | const plain = toUTF8Array(str); 52 | const buf = new ArrayBuffer(plain.length); 53 | const arr = new Uint8Array(buf); 54 | for(let i = 0; i> 4; 76 | 77 | if(heading < 8) // ASCII 78 | result += String.fromCharCode(char); 79 | else if(heading < 14) { // 2-byte 80 | ++i; 81 | const char2 = arr[i]; 82 | 83 | result += String.fromCharCode( 84 | ((char & 0x1F) << 6) | (char2 & 0x3F) 85 | ); 86 | } else { 87 | // 3-byte 88 | ++i; 89 | const char2 = arr[i]; 90 | ++i; 91 | const char3 = arr[i]; 92 | 93 | result += String.fromCharCode( 94 | ((char & 0x0F) << 12) | 95 | ((char2 & 0x3F) << 6) | 96 | ((char3 & 0x3F) << 0) 97 | ); 98 | } 99 | } 100 | 101 | return result; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /frontend/src/prelude.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: inherit; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | box-sizing: border-box; 8 | 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 11 | sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | 15 | background: #333; 16 | } 17 | 18 | .global-loading { 19 | position: absolute; 20 | bottom: 0; 21 | top: 0; 22 | left: 0; 23 | right: 0; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | flex-direction: column; 28 | 29 | z-index: 0; 30 | 31 | $items: 10; 32 | $step: 4s; 33 | $interleaving: .5s; 34 | $height: 30px; 35 | 36 | .slider-wrapper { 37 | font-family: 'Roboto Mono', monospace; 38 | position: relative; 39 | height: $height; 40 | width: 300px; 41 | margin-top: 20px; 42 | } 43 | 44 | .slider { 45 | font-size: 12px; 46 | position: absolute; 47 | top: 0; 48 | bottom: 0; 49 | left: 0; 50 | right: 0; 51 | line-height: $height; 52 | text-align: center; 53 | background: rgba(0,0,0,.18); 54 | color: rgba(255,255,255,.7); 55 | 56 | animation: slider-animation (($step - $interleaving) * $items) ease infinite; 57 | 58 | clip-path: inset(0 100% 0 0); 59 | 60 | &:after { 61 | content: '...'; 62 | animation: slider-tail-animation (($step - $interleaving) * $items) ease infinite; 63 | } 64 | 65 | @for $i from 1 through $items { 66 | &:nth-child(#{$i}) { 67 | animation-delay: ($step - $interleaving) * ($i - 1); 68 | 69 | &:after { 70 | animation-delay: ($step - $interleaving) * ($i - 1); 71 | } 72 | } 73 | } 74 | } 75 | 76 | $factor: $step / ($step - $interleaving) / $items; 77 | 78 | @keyframes slider-animation { 79 | 0% { 80 | clip-path: inset(0 100% 0 0); 81 | transform: translateX(-10px); 82 | } 83 | 84 | #{(20% * $factor)}, #{(80% * $factor)} { 85 | clip-path: inset(0); 86 | transform: translateX(0); 87 | } 88 | 89 | #{(100% * $factor)}, 100% { 90 | clip-path: inset(0 0 0 100%); 91 | transform: translateX(10px); 92 | } 93 | } 94 | 95 | @keyframes slider-tail-animation { 96 | 0%, #{(25% * $factor)} { 97 | opacity: 1; 98 | } 99 | 100 | #{(30.1% * $factor)}, #{(35% * $factor)} { 101 | opacity: 0; 102 | } 103 | 104 | #{(35.1% * $factor)}, #{(40% * $factor)} { 105 | opacity: 1; 106 | } 107 | 108 | #{(40.1% * $factor)}, #{(45% * $factor)} { 109 | opacity: 0; 110 | } 111 | 112 | #{(45.1% * $factor)}, 100% { 113 | opacity: 1; 114 | } 115 | } 116 | } 117 | 118 | .loading { 119 | width: 40px; 120 | height: 40px; 121 | position: relative; 122 | 123 | border: #121212 5px solid; 124 | box-sizing: border-box; 125 | border-radius: 100%; 126 | 127 | &:before, &:after { 128 | content: ''; 129 | position: absolute; 130 | 131 | left: -5px; 132 | right: -5px; 133 | top: -5px; 134 | bottom: -5px; 135 | 136 | border-radius: 100%; 137 | border: transparent 5px solid; 138 | } 139 | 140 | &:before { 141 | border-top-color: #FFC738; 142 | animation: rotate infinite ease 3.5s; 143 | } 144 | 145 | &:after { 146 | border-bottom-color: #57FFF2; 147 | 148 | animation: rotate infinite linear 3s; 149 | 150 | mix-blend-mode: exclusion; 151 | } 152 | 153 | @keyframes rotate { 154 | 0% { 155 | transform: rotate(0deg); 156 | } 157 | 158 | 100% { 159 | transform: rotate(720deg); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /frontend/src/routes/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import cn from 'classnames'; 5 | 6 | import { login, restore } from '../store/actions'; 7 | import { BACKEND } from '../config'; 8 | 9 | import Input from '../comps/Input'; 10 | import Icon from '../comps/Icon'; 11 | 12 | export default React.memo(() => { 13 | const [user, setUser] = useState(''); 14 | const [pass, setPass] = useState(''); 15 | const [errored, setErrored] = useState(false); 16 | 17 | const changeUser = useCallback(v => { 18 | setErrored(false); 19 | setUser(v); 20 | }, []); 21 | 22 | const changePass = useCallback(v => { 23 | setErrored(false); 24 | setPass(v); 25 | }, []); 26 | 27 | const navigate = useNavigate(); 28 | 29 | const dispatch = useDispatch(); 30 | const doLogin = useCallback(async () => { 31 | const success = await dispatch(login(user, pass)); 32 | if(success) 33 | navigate("/"); 34 | else 35 | setErrored(true); 36 | }, [dispatch, user, pass, navigate]); 37 | 38 | const [portalOngoing, setPortalOngoing] = useState(false); 39 | const doPortalAuth = useCallback(() => { 40 | const loginWindow = window.open(BACKEND + '/api/portal/auth'); 41 | setPortalOngoing(true); 42 | const cb = async (ev) => { 43 | if(ev.data !== 'done') return; 44 | loginWindow.close(); 45 | window.removeEventListener('message', cb); 46 | const success = await dispatch(restore()) 47 | setPortalOngoing(false); 48 | 49 | if(success) 50 | navigate("/"); 51 | else 52 | setErrored(true); 53 | }; 54 | window.addEventListener('message', cb); 55 | }, [dispatch, navigate]); 56 | 57 | const checkEnter = useCallback(ev => { 58 | if(ev.key === 'Enter') doLogin(); 59 | }, [doLogin]); 60 | 61 | return
62 |
63 | 69 |
70 |
71 | 72 |
73 | 74 | 75 | 80 |
81 |
82 |
; 83 | }); 84 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | if(registration.waiting && config && config.onUpdate) { 62 | // There is already a guy hanging out at waiting state 63 | config.onUpdate(registration); 64 | } 65 | 66 | registration.onupdatefound = () => { 67 | const installingWorker = registration.installing; 68 | if (installingWorker == null) { 69 | return; 70 | } 71 | installingWorker.onstatechange = () => { 72 | if (installingWorker.state === 'installed') { 73 | if (navigator.serviceWorker.controller) { 74 | // At this point, the updated precached content has been fetched, 75 | // but the previous service worker will still serve the older 76 | // content until all client tabs are closed. 77 | console.log( 78 | 'New content is available and will be used when all ' + 79 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 80 | ); 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration); 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It's the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log('Content is cached for offline use.'); 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration); 95 | } 96 | } 97 | } 98 | }; 99 | }; 100 | 101 | // Check for update periodically 102 | registration.update(); 103 | if(window.swUpdateInterval) clearInterval(window.swUpdateInterval); 104 | window.swUpdateInterval = setInterval(() => { 105 | try { 106 | registration.update(); 107 | } catch(e) { 108 | console.error('Registration failed: ' + e); 109 | } 110 | }, 5 * 60 * 1000) 111 | }) 112 | .catch(error => { 113 | console.error('Error during service worker registration:', error); 114 | }); 115 | } 116 | 117 | function checkValidServiceWorker(swUrl, config) { 118 | // Check if the service worker can be found. If it can't reload the page. 119 | fetch(swUrl, { 120 | headers: { 'Service-Worker': 'script' } 121 | }) 122 | .then(response => { 123 | // Ensure service worker exists, and that we really are getting a JS file. 124 | const contentType = response.headers.get('content-type'); 125 | if ( 126 | response.status === 404 || 127 | (contentType != null && contentType.indexOf('javascript') === -1) 128 | ) { 129 | // No service worker found. Probably a different app. Reload the page. 130 | navigator.serviceWorker.ready.then(registration => { 131 | registration.unregister().then(() => { 132 | window.location.reload(); 133 | }); 134 | }); 135 | } else { 136 | // Service worker found. Proceed as normal. 137 | registerValidSW(swUrl, config); 138 | } 139 | }) 140 | .catch(() => { 141 | console.log( 142 | 'No internet connection found. App is running in offline mode.' 143 | ); 144 | }); 145 | } 146 | 147 | export function unregister() { 148 | if ('serviceWorker' in navigator) { 149 | navigator.serviceWorker.ready.then(registration => { 150 | registration.unregister(); 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { compose, createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | 5 | import * as Sentry from '@sentry/react'; 6 | 7 | import * as reducers from './reducers'; 8 | 9 | import { Map, List } from 'immutable'; 10 | import { DEFAULT_BOARD, DEFAULT_FIELD } from '../config'; 11 | 12 | const init = () => { 13 | const code = window.localStorage.getItem('code') || ''; 14 | const board = window.localStorage.getItem('board') || DEFAULT_BOARD; 15 | const top = window.localStorage.getItem('top') || null; 16 | const signals = Map(JSON.parse(window.localStorage.getItem('signals')) || {}); 17 | const lang = window.localStorage.getItem('lang') || 'vhdl'; 18 | const field = List(JSON.parse(window.localStorage.getItem('field')) || DEFAULT_FIELD); 19 | 20 | let middleware = [thunk]; 21 | if (process.env.NODE_ENV !== 'production') { 22 | middleware = [...middleware, logger] 23 | } 24 | 25 | const sentryEnhancer = Sentry.createReduxEnhancer({}); 26 | 27 | const store = createStore( 28 | combineReducers(reducers), 29 | { 30 | code, 31 | constraints: { 32 | board, 33 | top, 34 | signals, 35 | }, 36 | lang, 37 | field, 38 | }, 39 | compose(applyMiddleware(...middleware), sentryEnhancer), 40 | ); 41 | 42 | function saver(name, first, mapper = data => data) { 43 | let last = first; 44 | return data => { 45 | if(data !== last) { 46 | last = data; 47 | 48 | const mapped = mapper(data); 49 | window.localStorage.setItem(name, mapped); 50 | } 51 | } 52 | } 53 | 54 | const saveCode = saver('code', code); 55 | const saveBoard = saver('board', board); 56 | const saveTop = saver('top', top); 57 | const saveSignals = saver('signals', signals, d => JSON.stringify(d.toJS())); 58 | const saveLang = saver('lang', lang); 59 | const saveField = saver('field', field, d => JSON.stringify(d.toJS())); 60 | 61 | store.subscribe(() => { 62 | const { code, constraints: { board, top, signals }, lang, field } = store.getState(); 63 | 64 | saveCode(code); 65 | saveBoard(board); 66 | saveTop(top); 67 | saveSignals(signals); 68 | saveLang(lang); 69 | saveField(field); 70 | }); 71 | 72 | return store; 73 | }; 74 | 75 | export default init; 76 | -------------------------------------------------------------------------------- /frontend/src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { TYPES, BOARD_STATUS } from './actions'; 2 | import { Map, List } from 'immutable'; 3 | import { DEFAULT_BOARD, BOARDS, DEFAULT_FIELD } from '../config'; 4 | import { SIGNAL } from '../blocks'; 5 | 6 | export function user(state = null, action) { 7 | if(action.type === TYPES.SET_USER) 8 | return action.user; 9 | return state; 10 | } 11 | 12 | export function lib(state = null, action) { 13 | if(action.type === TYPES.LOAD_LIB) 14 | return action.lib; 15 | return state; 16 | } 17 | 18 | export function code(state = null, action) { 19 | if(action.type === TYPES.SET_CODE) 20 | return action.code; 21 | return state; 22 | } 23 | 24 | export function analysis(state = null, action) { 25 | if(action.type === TYPES.SET_ANALYSIS) 26 | return action.analysis; 27 | return state; 28 | } 29 | 30 | export function builds(state = { list: List(), ended: false }, action) { 31 | if(action.type === TYPES.LOAD_BUILDS) 32 | return { list: action.list, ended: action.ended }; 33 | if(action.type === TYPES.PUT_BUILD) { 34 | const { build } = action; 35 | const { id } = build; 36 | if(!id) throw new Error('Build missing field id'); 37 | 38 | const { list, ended } = state; 39 | 40 | const idx = list.findIndex(e => e.id === id); 41 | if(idx === -1) return { list: list.unshift(build), ended }; 42 | else return { list: list.set(idx, build), ended }; 43 | } 44 | return state; 45 | } 46 | 47 | export function activeBuild(state = null, action) { 48 | if(action.type === TYPES.SET_ACTIVE_BUILD) 49 | return action.build; 50 | return state; 51 | } 52 | 53 | export function board(state = { status: BOARD_STATUS.DISCONNECTED, ident: null, websocket: null }, action) { 54 | if(action.type === TYPES.SET_BOARD) 55 | return action.board; 56 | if(action.type === TYPES.UPDATE_BOARD) 57 | return { 58 | ...state, 59 | status: action.status, 60 | }; 61 | return state; 62 | } 63 | 64 | export function constraints(state = { board: DEFAULT_BOARD, top: null, signals: new Map() }, action) { 65 | if(action.type === TYPES.SELECT_BOARD) { 66 | return { board: action.board, top: null, signals: new Map() } 67 | } else if(action.type === TYPES.ASSIGN_TOP) { 68 | return { board: state.board, top: action.top, signals: new Map() }; 69 | } else if(action.type === TYPES.ASSIGN_PIN) { 70 | const { signal, pin } = action; 71 | const { board, top } = state; 72 | 73 | return { 74 | board, top, 75 | signals: state.signals.filter(v => v !== pin).set(signal, pin), 76 | }; 77 | } else if(action.type === TYPES.SET_ANALYSIS) { 78 | const { analysis } = action; 79 | if(analysis.top === null) return state; 80 | 81 | const entity = analysis.entities[analysis.top]; 82 | const board = BOARDS[state.board]; 83 | 84 | let mapper = new Map(); 85 | for(const signal of entity.signals) mapper = mapper.set(signal.name, signal); 86 | 87 | return { 88 | board: state.board, 89 | top: state.top, 90 | 91 | signals: state.signals.filter((v, k) => { 92 | const regex = /^([^[\]]+)(\[([0-9]+)\])?$/; 93 | // Asserts to match 94 | const [, base,, subscript] = k.match(regex); 95 | const lookup = mapper.get(base); 96 | if(!lookup) return false; 97 | const { dir, arity } = lookup; 98 | if(dir === undefined) return false; 99 | 100 | if(subscript !== undefined) { 101 | const numSub = Number.parseInt(subscript, 10); 102 | // Signal not an array 103 | if(arity === null) 104 | return false; 105 | if(arity.from <= arity.to && ( 106 | arity.from > numSub || 107 | arity.to < numSub 108 | )) 109 | return false; 110 | else if(arity.from > arity.to && ( 111 | arity.from < numSub || 112 | arity.to > numSub 113 | )) 114 | return false; 115 | } else { 116 | // Signal changed into array 117 | if(arity !== null) 118 | return false; 119 | } 120 | 121 | const spec = board.pins[v]; 122 | 123 | if(!spec) return false; 124 | 125 | if(dir === 'input' && !spec.output) return false; 126 | if(dir === 'output' && !spec.input) return false; 127 | 128 | return true; 129 | }), 130 | } 131 | } 132 | 133 | return state; 134 | } 135 | 136 | export function clock(state = null, action) { 137 | if(action.type === TYPES.SET_CLOCK) 138 | return action.clock; 139 | return state; 140 | } 141 | 142 | export function input(state = null, action) { 143 | if(action.type === TYPES.UPDATE_INPUT) { 144 | const { data } = action; 145 | return data.split('').map(e => e === '1' ? SIGNAL.H : SIGNAL.L); 146 | } 147 | 148 | return state; 149 | } 150 | 151 | export function help(state = null, action) { 152 | if(action.type === TYPES.STEP_HELP) 153 | return state + 1; 154 | else if(action.type === TYPES.UNSTEP_HELP) 155 | return state === 0 ? 0 : state - 1; 156 | else if(action.type === TYPES.END_HELP) 157 | return null; 158 | else if(action.type === TYPES.START_HELP) 159 | return 0; 160 | return state; 161 | } 162 | 163 | export function field(state = List(DEFAULT_FIELD), action) { 164 | if(action.type === TYPES.LOAD_FIELD) 165 | return action.field; 166 | else if(action.type === TYPES.SETTLE_BLOCK) 167 | return state.set(action.idx, { ...state.get(action.idx), x: action.x, y: action.y }); 168 | else if(action.type === TYPES.DELETE_BLOCK) 169 | return state.delete(action.idx); 170 | else if(action.type === TYPES.PUSH_BLOCK) 171 | return state.push(action.block); 172 | return state; 173 | } 174 | 175 | export function lang(state = 'vhdl', action) { 176 | if(action.type === TYPES.SET_LANG) 177 | return action.lang; 178 | return state; 179 | } 180 | 181 | export function snackbar(state = List(), action) { 182 | if(action.type === TYPES.PUSH_SNACKBAR) 183 | return state.push({ id: action.id, spec: action.spec }); 184 | else if(action.type === TYPES.POP_SNACKBAR) { 185 | const idx = state.findIndex(e => e.id === action.id); 186 | if(idx === -1) return state; 187 | return state.delete(idx); 188 | } 189 | return state; 190 | } 191 | -------------------------------------------------------------------------------- /frontend/src/styles/consts.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #4068e0; 2 | $error-color: change-color($primary-color, $hue: 0); 3 | $selected-color: #FFC738; 4 | $bg: #333; 5 | -------------------------------------------------------------------------------- /frontend/src/styles/editor.scss: -------------------------------------------------------------------------------- 1 | $warning: #D4CE24; 2 | $confirm: #29C2BD; 3 | $error: #CB4042; 4 | 5 | .unassigned-signal-line { 6 | background: fade-out($warning, 0.9); 7 | } 8 | 9 | .unassigned-signal-glyph { 10 | background: $warning; 11 | } 12 | 13 | .assigned-signal-glyph { 14 | background: fade-out($confirm, 0.5); 15 | } 16 | 17 | .top-line { 18 | background: fade-out($confirm, 0.9); 19 | } 20 | 21 | .top-glyph { 22 | background: $confirm; 23 | } 24 | 25 | .diagnostic-error { 26 | text-decoration: underline; 27 | text-decoration-thickness: 2px; 28 | // text-decoration-style: wavy; 29 | text-decoration-color: $error; 30 | text-decoration-skip-ink: none; 31 | background: fade-out($error, 0.8); 32 | } 33 | 34 | .diagnostic-warning { 35 | text-decoration: underline; 36 | // text-decoration-style: wavy; 37 | text-decoration-color: $warning; 38 | text-decoration-skip-ink: none; 39 | } 40 | 41 | .diagnostic-info { 42 | text-decoration: underline; 43 | // text-decoration-style: wavy; 44 | text-decoration-color: $confirm; 45 | text-decoration-skip-ink: none; 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/styles/error.scss: -------------------------------------------------------------------------------- 1 | .error { 2 | position: fixed; 3 | bottom: 0; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | background: $bg; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | flex-direction: column; 12 | 13 | color: white; 14 | 15 | &-title { 16 | font-size: 32px; 17 | font-weight: 700; 18 | } 19 | 20 | &-subtitle { 21 | font-size: 18px; 22 | opacity: .3; 23 | margin-top: 10px; 24 | } 25 | 26 | &-desc { 27 | margin-top: 40px; 28 | max-width: 400px; 29 | text-indent: 2em; 30 | margin-bottom: 20px; 31 | } 32 | 33 | &-progress { 34 | margin-top: 20px; 35 | height: 5px; 36 | background: rgba(0,0,0,.12); 37 | width: calc(100% - 100px); 38 | max-width: 400px; 39 | 40 | overflow: hidden; 41 | 42 | &:after { 43 | content: ' '; 44 | display: block; 45 | height: 100%; 46 | width: 100%; 47 | 48 | background: $primary-color; 49 | 50 | animation: error-progress 1.5s ease infinite; 51 | 52 | @keyframes error-progress { 53 | 0% { 54 | transform: translateX(-100%); 55 | } 56 | 57 | 100% { 58 | transform: translateX(100%); 59 | } 60 | } 61 | } 62 | } 63 | 64 | &-evid { 65 | opacity: .18; 66 | position: fixed; 67 | bottom: 20px; 68 | font-size: 12px; 69 | left: 0; 70 | right: 0; 71 | text-align: center; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/styles/font.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 100; 5 | font-display: swap; 6 | src: url(../fonts/Roboto/Roboto-Thin.woff2) format('woff2'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Roboto'; 11 | font-style: normal; 12 | font-weight: 400; 13 | font-display: swap; 14 | src: url(../fonts/Roboto/Roboto-Regular.woff2) format('woff2'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Roboto'; 19 | font-style: normal; 20 | font-weight: 700; 21 | font-display: swap; 22 | src: url(../fonts/Roboto/Roboto-Bold.woff2) format('woff2'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'Roboto Mono'; 27 | font-style: normal; 28 | font-weight: 100; 29 | font-display: swap; 30 | src: url(../fonts/RobotoMono/RobotoMono-Thin.woff2) format('woff2'); 31 | } 32 | 33 | @font-face { 34 | font-family: 'Roboto Mono'; 35 | font-style: normal; 36 | font-weight: 400; 37 | font-display: swap; 38 | src: url(../fonts/RobotoMono/RobotoMono-Regular.woff2) format('woff2'); 39 | } 40 | 41 | @font-face { 42 | font-family: 'Roboto Mono'; 43 | font-style: normal; 44 | font-weight: 700; 45 | font-display: swap; 46 | src: url(../fonts/RobotoMono/RobotoMono-Bold.woff2) format('woff2'); 47 | } 48 | 49 | @font-face { 50 | font-family: 'Material Icons'; 51 | font-style: normal; 52 | font-weight: 400; 53 | src: url(../fonts/MaterialIcons/MaterialIcons.woff2) format('woff2'); 54 | } 55 | 56 | .material-icons { 57 | font-family: 'Material Icons'; 58 | font-weight: normal; 59 | font-style: normal; 60 | font-size: 24px; 61 | line-height: 1; 62 | letter-spacing: normal; 63 | text-transform: none; 64 | display: inline-block; 65 | white-space: nowrap; 66 | word-wrap: normal; 67 | direction: ltr; 68 | -webkit-font-feature-settings: 'liga'; 69 | -webkit-font-smoothing: antialiased; 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/styles/help.scss: -------------------------------------------------------------------------------- 1 | $hl: #ffeb3b; 2 | 3 | .help { 4 | position: fixed; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | top: 0; 9 | padding: 40px; 10 | 11 | pointer-events: none; 12 | 13 | z-index: 120; 14 | 15 | color: white; 16 | letter-spacing: 0.05em; 17 | } 18 | 19 | .help .brand { 20 | font-size: 48px; 21 | color: white; 22 | line-height: 48px; 23 | 24 | strong { 25 | font-size: inherit; 26 | display: inline-block; 27 | line-height: inherit; 28 | } 29 | } 30 | 31 | .help-backdrop { 32 | position: absolute; 33 | bottom: 0; 34 | left: 0; 35 | right: 0; 36 | top: 0; 37 | z-index: 0; 38 | 39 | background: rgba(0,0,0,.3); 40 | opacity: 0; 41 | transition: opacity .2s ease, transform .2s ease; 42 | 43 | .help-backdrop-stripe { 44 | $overlay: rgba(0,0,0,.18); 45 | background-image: linear-gradient(45deg, $overlay 0% 25%, transparent 25% 50%, $overlay 50% 75%, transparent 75% 100%); 46 | position: absolute; 47 | bottom: 0; 48 | left: 0; 49 | right: 0; 50 | top: 0; 51 | background-size: 100px 100px; 52 | 53 | opacity: 0; 54 | transition: opacity .2s ease; 55 | } 56 | 57 | &.help-backdrop-shown { 58 | opacity: 1; 59 | pointer-events: all; 60 | } 61 | 62 | &.help-backdrop-editor-only { 63 | transform: translateX(-50vw); 64 | .help-backdrop-stripe { 65 | opacity: 1; 66 | } 67 | } 68 | 69 | &.help-backdrop-sandbox-only { 70 | transform: translateX(50vw); 71 | .help-backdrop-stripe { 72 | opacity: 1; 73 | } 74 | } 75 | } 76 | 77 | .help-controller-cont { 78 | position: fixed; 79 | 80 | right: 0; 81 | width: 60px; 82 | top: 0; 83 | bottom: 0; 84 | 85 | display: flex; 86 | align-items: center; 87 | 88 | transform: translateX(60px + 12px); 89 | transition: transform .2s ease; 90 | 91 | z-index: 120; 92 | } 93 | 94 | .help-controller { 95 | flex: 1; 96 | height: auto; 97 | background: $bg; 98 | box-shadow: rgba(0,0,0,.3) 0 4px 12px; 99 | display: inline-flex; 100 | flex-direction: column; 101 | 102 | pointer-events: all; 103 | } 104 | 105 | .help-open .help-controller-cont { 106 | transform: none; 107 | } 108 | 109 | .help-action { 110 | height: 60px; 111 | width: 60px; 112 | line-height: 60px; 113 | text-align: center; 114 | color: white; 115 | 116 | cursor: pointer; 117 | 118 | opacity: .7; 119 | transition: opacity .2s ease; 120 | 121 | &:hover { 122 | opacity: 1; 123 | } 124 | 125 | &.help-action-disabled { 126 | cursor: default; 127 | opacity: .18; 128 | } 129 | } 130 | 131 | .help-action-primary { 132 | background: $primary-color; 133 | transition: filter .2s ease; 134 | 135 | &.help-action-primary-disabled { 136 | filter: grayscale(1) brightness(0.7); 137 | } 138 | } 139 | 140 | .help-content { 141 | transition: opacity .2s ease; 142 | 143 | &.help-content-hidden { 144 | opacity: 0; 145 | } 146 | } 147 | 148 | .help del { 149 | margin: 0 5px; 150 | color: rgba(255,255,255,.3); 151 | text-decoration-color: white; 152 | } 153 | 154 | .help p { 155 | margin-bottom: 0; 156 | color: rgba(255,255,255,.7); 157 | } 158 | 159 | .help-welcome { 160 | position: absolute; 161 | left: 0; 162 | right: 0; 163 | top: 0; 164 | bottom: 0; 165 | 166 | display: flex; 167 | align-items: center; 168 | justify-content: center; 169 | flex-direction: column; 170 | 171 | .help-welcome-inner { 172 | padding: 30px 40px; 173 | background: rgba(0,0,0,1); 174 | } 175 | 176 | .help-welcome-desc { 177 | margin-top: 40px; 178 | max-width: 400px; 179 | } 180 | 181 | .help-welcome-icons { 182 | margin-top: 40px; 183 | display: flex; 184 | align-items: center; 185 | justify-content: center; 186 | 187 | .help-welcome-icon { 188 | font-size: 12px; 189 | color: rgba(255,255,255,.54); 190 | display: flex; 191 | flex-direction: column; 192 | align-items: center; 193 | justify-content: center; 194 | 195 | .material-icons { 196 | color: white; 197 | } 198 | } 199 | } 200 | } 201 | 202 | .help-box { 203 | max-width: 400px; 204 | padding: 30px 40px; 205 | 206 | background: rgba(0,0,0,1); 207 | z-index: 1; 208 | 209 | strong { 210 | display: block; 211 | font-size: 32px; 212 | line-height: 32px; 213 | } 214 | 215 | &.help-box-wide { 216 | max-width: 600px; 217 | } 218 | 219 | &.with-events { 220 | .help-shown & { 221 | pointer-events: all; 222 | } 223 | } 224 | } 225 | 226 | .help-layout { 227 | .help-layout-base { 228 | display: flex; 229 | position: absolute; 230 | top: 60px; 231 | bottom: 0; 232 | right: 0; 233 | left: 0; 234 | 235 | .help-layout-half { 236 | flex: 1; 237 | height: 100%; 238 | display: flex; 239 | align-items: center; 240 | flex-direction: column; 241 | justify-content: center; 242 | 243 | padding: 20px; 244 | } 245 | } 246 | 247 | .help-layout-toolbar { 248 | position: absolute; 249 | top: 60px; 250 | left: calc(50vw - 60px); 251 | 252 | .help-layout-row { 253 | height: 60px; 254 | display: flex; 255 | 256 | .material-icons { 257 | width: 60px; 258 | height: 60px; 259 | 260 | display: inline-flex; 261 | align-items: center; 262 | justify-content: center; 263 | 264 | border: white 1px dashed; 265 | } 266 | 267 | .help-layout-desc { 268 | display: inline-flex; 269 | height: 60px; 270 | align-items: center; 271 | padding: 0 20px; 272 | background: rgba(0,0,0,1); 273 | } 274 | } 275 | } 276 | 277 | .help-layout-build { 278 | position: absolute; 279 | top: 0; 280 | right: 70px; 281 | height: 60px; 282 | width: 120px; 283 | display: flex; 284 | align-items: center; 285 | justify-content: center; 286 | border: white 1px dashed; 287 | background: rgba(0,0,0,1); 288 | } 289 | } 290 | 291 | .help .help-hl { 292 | color: $hl; 293 | } 294 | -------------------------------------------------------------------------------- /frontend/src/styles/highlighter.scss: -------------------------------------------------------------------------------- 1 | $warning: #ffeb3b; 2 | $error: #ef5350; 3 | $comment: #7cb342; 4 | 5 | .highlighter { 6 | @extend .monospace; 7 | font-size: 14px; 8 | line-height: 18px; 9 | 10 | .highlighter-comment { 11 | color: $comment; 12 | } 13 | 14 | .highlighter-warning { 15 | color: $warning; 16 | } 17 | 18 | .highlighter-error { 19 | color: $error; 20 | } 21 | 22 | code { 23 | word-break: break-all; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/styles/scrollbar.scss: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar-thumb { 2 | background: rgba(255,255,255,.2); 3 | } 4 | 5 | ::-webkit-scrollbar { 6 | width: 10px; 7 | height: 10px; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/styles/shutter.scss: -------------------------------------------------------------------------------- 1 | $tile-size: 150px; 2 | $shutter-color: white; 3 | $shutter-opacity: .3; 4 | $error-margin: .02; 5 | 6 | .shutter { 7 | position: absolute; 8 | bottom: 0; 9 | left: 0; 10 | right: 0; 11 | top: 0; 12 | 13 | pointer-events: none; 14 | opacity: $shutter-opacity; 15 | } 16 | 17 | .shutter-top { 18 | position: absolute; 19 | bottom: -$tile-size; 20 | left: -$tile-size; 21 | right: -$tile-size; 22 | top: -$tile-size; 23 | 24 | background: linear-gradient(45deg, $shutter-color 0% 20%, transparent 20% 50%, $shutter-color 50% 70%, transparent 70% 100%); 25 | background-size: $tile-size $tile-size; 26 | background-repeat: repeat; 27 | 28 | mask-image: linear-gradient(45deg, black 0% (20% + $error-margin * 100%), transparent (20% + $error-margin * 100%) 50%, black 50% (70% + $error-margin * 100%), transparent (70% + $error-margin * 100%) 100%); 29 | mask-size: $tile-size $tile-size; 30 | mask-repeat: repeat; 31 | mask-position: ($tile-size * .5 - ($error-margin)) 0px; 32 | 33 | transition: mask-position 1s ease; 34 | 35 | .shutter.shutter-open & { 36 | mask-position: ($tile-size * -$error-margin) 0px; 37 | } 38 | } 39 | 40 | .shutter-middle { 41 | @extend .shutter-top; 42 | transform: translateY($tile-size/3); 43 | } 44 | 45 | .shutter-bottom { 46 | @extend .shutter-top; 47 | transform: translateY($tile-size * 2/3); 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip-container { 2 | position: relative; 3 | display: inline-block; 4 | 5 | &:hover { 6 | z-index: 1000; 7 | } 8 | } 9 | 10 | .tooltip-hover { 11 | position: relative; 12 | z-index: 3; 13 | } 14 | 15 | .tooltip { 16 | position: absolute; 17 | pointer-events: none; 18 | 19 | z-index: 2; 20 | 21 | top: 0px; 22 | right: 100%; 23 | width: 200px; 24 | height: auto; 25 | min-height: 100%; 26 | min-width: 100%; 27 | 28 | background: rgba(0,0,0,.9); 29 | 30 | opacity: 0; 31 | transform: translateX(20px); 32 | 33 | transition: opacity .2s ease, transform .2s ease; 34 | 35 | .tooltip-hover:hover + &:not(.tooltip-disabled) { 36 | opacity: 1; 37 | transform: translateX(0px); 38 | 39 | &.tooltip-gap { 40 | transform: translateX(-10px); 41 | } 42 | } 43 | 44 | padding: 15px 20px; 45 | display: inline-flex; 46 | align-items: center; 47 | 48 | color: rgba(255,255,255, .7); 49 | font-size: 12px; 50 | line-height: 12px; 51 | border-right: change-color($primary-color, $alpha: 0.5) 5px solid; 52 | @extend .monospace; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | .fade-enter { 2 | opacity: 0; 3 | } 4 | 5 | .fade-enter-active { 6 | transition: opacity .2s ease-out; 7 | opacity: 1; 8 | } 9 | 10 | .fade-exit { 11 | opacity: 1; 12 | } 13 | 14 | .fade-exit-active { 15 | transition: opacity .2s ease-in; 16 | opacity: 0; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/util.js: -------------------------------------------------------------------------------- 1 | import { BACKEND } from './config'; 2 | 3 | class NetworkError extends Error { 4 | constructor({ msg, code }) { 5 | super(msg); 6 | this.code = code; 7 | } 8 | } 9 | 10 | async function parseResp(resp) { 11 | if (resp.status >= 400) 12 | throw new NetworkError({ 13 | code: resp.status, 14 | msg: await resp.text(), 15 | }); 16 | return await resp.json(); 17 | } 18 | 19 | export async function get(endpoint, method = "GET") { 20 | const resp = await fetch(BACKEND + endpoint, { 21 | method, 22 | credentials: 'include', 23 | }); 24 | 25 | return await parseResp(resp); 26 | } 27 | 28 | export async function post(endpoint, data, method = "POST") { 29 | const resp = await fetch(BACKEND + endpoint, { 30 | method, 31 | credentials: 'include', 32 | body: JSON.stringify(data), 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | } 36 | }); 37 | 38 | return await parseResp(resp); 39 | } 40 | 41 | export async function putS3(endpoint, data, method = "PUT") { 42 | const resp = await fetch(endpoint, { 43 | method, 44 | body: data, 45 | mode: 'cors', 46 | }); 47 | 48 | return await resp.text(); 49 | } 50 | 51 | function setString(array, str, offset) { 52 | for (let i = 0; i < str.length; i++) { 53 | array[offset + i] = str.charCodeAt(i); 54 | } 55 | } 56 | 57 | function octal(num, bytes) { 58 | num = num.toString(8); 59 | return "0".repeat(bytes - num.length) + num + " "; 60 | } 61 | 62 | function createTarOneFile(name, body) { 63 | let encoder = new TextEncoder(); 64 | let bodyArray = encoder.encode(body); 65 | let totalLength = 512 + Math.floor((bodyArray.length + 511) / 512) * 512; 66 | let file = new Uint8Array(totalLength); 67 | setString(file, name, 0); 68 | // mode 69 | setString(file, octal(0o644, 6), 100); 70 | // owner 71 | setString(file, octal(0, 6), 108); 72 | // group 73 | setString(file, octal(0, 6), 116); 74 | // length 75 | let length = bodyArray.length; 76 | setString(file, octal(length, 11), 124); 77 | // mod time 78 | setString(file, octal(0, 11), 136); 79 | // checksum 80 | let checksum = 0; 81 | for (let i = 0; i < 148; i += 1) { 82 | checksum += file[i]; 83 | } 84 | checksum += 8 * 32;// 8 spaces 85 | checksum = checksum.toString(8); 86 | setString(file, "0".repeat(6 - checksum.length) + checksum + "\u0000 ", 148); 87 | file.set(bodyArray, 512); 88 | return file; 89 | } 90 | 91 | export function toBitArray(number) { 92 | let res = []; 93 | for (let i = 0; i < 64; i++) { 94 | res[i] = number % 2; 95 | number = Math.floor(number / 2); 96 | } 97 | return res; 98 | } 99 | 100 | export function createTarFile(files) { 101 | let parts = files.map((file) => { 102 | let { name, body } = file; 103 | return createTarOneFile(name, body); 104 | }); 105 | let totalLength = 0; 106 | parts.map((part) => totalLength += part.length); 107 | totalLength += 2 * 512; 108 | let file = new Uint8Array(totalLength); 109 | let offset = 0; 110 | for (let i = 0; i < parts.length; i++) { 111 | file.set(parts[i], offset); 112 | offset += parts[i].length; 113 | } 114 | return file; 115 | } 116 | 117 | function parseTarEntry(tar, at) { 118 | const decoder = new TextDecoder(); 119 | const headerBlk = tar.slice(at, at + 512); 120 | 121 | if(at >= tar.length - 512*2) 122 | return null; 123 | if(headerBlk.every(e => e === 0) && tar.slice(at+512, at+1024).every(e => e === 0)) 124 | return null; 125 | 126 | // Read name 127 | const nameDelim = headerBlk.findIndex(e => e === 0); 128 | const name = decoder.decode(headerBlk.slice(0, nameDelim)); 129 | 130 | // Read length 131 | const lenStr = decoder.decode(headerBlk.slice(124, 124+12)); 132 | const len = Number.parseInt(lenStr, 8); 133 | if(Number.isNaN(len)) throw new Error(`Unexpected len ${lenStr}`); 134 | 135 | const paddedLen = Math.ceil(len / 512) * 512; 136 | 137 | return { 138 | name, 139 | len, 140 | paddedLen, 141 | content: tar.slice(at + 512, at + 512 + len), 142 | }; 143 | } 144 | 145 | export function untar(tar) { 146 | // Parse headers 147 | let currentAt = 0; 148 | const result = []; 149 | while(true) { 150 | const entry = parseTarEntry(tar, currentAt); 151 | if(!entry) break; 152 | 153 | currentAt += 512 + entry.paddedLen; 154 | result.push(entry); 155 | } 156 | 157 | return result; 158 | } 159 | 160 | export function readFileStr(parsedTar, fn) { 161 | const decoder = new TextDecoder(); 162 | const raw = parsedTar.find(e => e.name === fn).content; 163 | return decoder.decode(raw); 164 | } 165 | 166 | const SIZE_SUFFIXS = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; 167 | export function formatSize(size) { 168 | let level = 0; 169 | while(size >= 1024) { 170 | level += 1; 171 | size = size / 1024; 172 | } 173 | 174 | if(SIZE_SUFFIXS.length <= level) return 'Meow?'; 175 | 176 | size = Math.round(size * 10) / 10; 177 | return `${size}${SIZE_SUFFIXS[level]}`; 178 | } 179 | 180 | export function formatDuration(dur, showMs = false) { 181 | // Start from ms 182 | let acc = ''; 183 | 184 | if(dur < 1000 || showMs) acc = `${dur % 1000}ms` 185 | 186 | dur = Math.floor(dur / 1000); 187 | if(dur === 0) return acc; 188 | 189 | acc = `${dur % 60}s ${acc}`; 190 | dur = Math.floor(dur / 60); 191 | if(dur === 0) return acc; 192 | 193 | acc = `${dur % 60}m ${acc}`; 194 | dur = Math.floor(dur / 60); 195 | if(dur === 0) return acc; 196 | 197 | acc = `${dur % 60}h ${acc}`; 198 | dur = Math.floor(dur / 24); 199 | if(dur === 0) return acc; 200 | 201 | return `${dur}d ${acc}` 202 | } 203 | 204 | export function formatDate(date) { 205 | return date.toLocaleString('zh-CN', { hour12: false }); 206 | } 207 | -------------------------------------------------------------------------------- /manage/.gitignore: -------------------------------------------------------------------------------- 1 | src/config.js 2 | 3 | .DS_Store 4 | node_modules 5 | /dist 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /manage/README.md: -------------------------------------------------------------------------------- 1 | # manage 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /manage/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /manage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manage", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.4", 12 | "vue": "^2.6.11", 13 | "vuetify": "^2.5.0" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.5.13", 17 | "@vue/cli-plugin-eslint": "^4.5.13", 18 | "@vue/cli-service": "^4.5.13", 19 | "babel-eslint": "^10.0.3", 20 | "eslint": "^7.22.0", 21 | "eslint-plugin-vue": "^7.7.0", 22 | "sass": "^1.32.13", 23 | "sass-loader": "10.1.1", 24 | "vue-cli-plugin-vuetify": "^2.4.0", 25 | "vue-template-compiler": "^2.6.11", 26 | "vuetify-loader": "^1.3.0" 27 | }, 28 | "eslintConfig": { 29 | "root": true, 30 | "env": { 31 | "node": true 32 | }, 33 | "extends": [ 34 | "plugin:vue/essential", 35 | "eslint:recommended" 36 | ], 37 | "parserOptions": { 38 | "parser": "babel-eslint" 39 | }, 40 | "rules": {} 41 | }, 42 | "browserslist": [ 43 | "> 1%", 44 | "last 2 versions" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /manage/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thu-cs-lab/JieLabs-Web/5e6ccc187af178f292dbf942fd5b93f45167fc07/manage/public/favicon.ico -------------------------------------------------------------------------------- /manage/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /manage/src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | -------------------------------------------------------------------------------- /manage/src/config.sample.js: -------------------------------------------------------------------------------- 1 | export const BACKEND = "http://localhost:8080"; -------------------------------------------------------------------------------- /manage/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import vuetify from './plugins/vuetify'; 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | vuetify, 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /manage/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /manage/src/util.js: -------------------------------------------------------------------------------- 1 | import { BACKEND } from './config'; 2 | 3 | async function parseResp(resp) { 4 | if (resp.status === 403) 5 | window.location.href = "/"; 6 | if (resp.status >= 400) 7 | throw { 8 | code: resp.code, 9 | msg: await resp.text(), 10 | }; 11 | return await resp.json(); 12 | } 13 | 14 | export async function get(endpoint, method = "GET") { 15 | const resp = await fetch(BACKEND + endpoint, { 16 | method, 17 | credentials: 'include', 18 | }); 19 | 20 | return await parseResp(resp); 21 | } 22 | 23 | export async function getLines(endpoint, method = "GET") { 24 | const resp = await fetch(BACKEND + endpoint, { 25 | method, 26 | credentials: 'include', 27 | }); 28 | 29 | if (resp.code >= 400) 30 | throw { 31 | code: resp.code, 32 | msg: resp.text(), 33 | }; 34 | return (await resp.text()).split("\n"); 35 | } 36 | 37 | export async function post(endpoint, data, method = "POST") { 38 | const resp = await fetch(BACKEND + endpoint, { 39 | method, 40 | credentials: 'include', 41 | body: JSON.stringify(data), 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | } 45 | }); 46 | 47 | return await parseResp(resp); 48 | } 49 | 50 | export async function put(endpoint, data, method = "PUT") { 51 | const resp = await fetch(BACKEND + endpoint, { 52 | method, 53 | credentials: 'include', 54 | body: JSON.stringify(data), 55 | headers: { 56 | 'Content-Type': 'application/json', 57 | } 58 | }); 59 | 60 | return await parseResp(resp); 61 | } 62 | 63 | export async function delete_(endpoint, method = "DELETE") { 64 | const resp = await fetch(BACKEND + endpoint, { 65 | method, 66 | credentials: 'include', 67 | }); 68 | 69 | return await parseResp(resp); 70 | } -------------------------------------------------------------------------------- /manage/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transpileDependencies": [ 3 | "vuetify" 4 | ], 5 | "publicPath": '/jie-manage' 6 | } 7 | -------------------------------------------------------------------------------- /protocol.md: -------------------------------------------------------------------------------- 1 | # 后端与板子通信协议 2 | 3 | 通过 WebSocket 通信,由板子发起,endpoint 为 /api/ws_board 。 4 | 5 | 客户端需要处理 WebSocket 自带的 PING 信息,并返回 PONG。如果一段时间没有返回 PONG,服务端会断开连接。 6 | 7 | 连接上第一步必须由客户端发起 Authenticate 请求,否则不能进行后续的请求。 8 | 9 | ## 请求格式 10 | 11 | ### 认证 12 | 13 | 客户端 -> 服务端 14 | 15 | 行为:向服务端发送一个预设的密码,以验证板子的身份。同时需要传递板子上软件和硬件的版本。 16 | 17 | 格式: 18 | 19 | ```json 20 | {"Authenticate":{"password":"password","software_version":"1.0","hardware_version":"0.1"}} 21 | ``` 22 | 23 | 认证后,如果服务端没有断开连接,则表明认证成功。 24 | 25 | ### Bitstream 编程结果 26 | 27 | 客户端 -> 服务端 28 | 29 | 行为:在向 FPGA 编程 Bitstream后,汇报编程结果。 30 | 31 | 格式: 32 | 33 | ```json 34 | {"ProgramBitstreamFinish":true} 35 | ``` 36 | 37 | 38 | 39 | ### 汇报 IO 状态更新 40 | 41 | 客户端 -> 服务端 42 | 43 | 行为:当服务端订阅了 IO 状态的更新时,如果 IO 状态相比上一次汇报有变更,则需要发送。 44 | 45 | 格式: 46 | 47 | ```json 48 | {"ReportIOChange":{"data":"0011010"}} 49 | ``` 50 | 51 | ### Bitstream 编程 52 | 53 | 服务端 -> 客户端 54 | 55 | 行为:通过 WebSocket Binary 消息。发送 Bitstream 并要求客户端编程 Bitstream。格式为 .tar.gz,bitstream 为内部的 bitstream.rbf 。 56 | 57 | 格式: 58 | 59 | ```json 60 | xxx.tar.gz: Tar in Gzip 61 | stdout 62 | stderr 63 | bitstream.rbf 64 | ``` 65 | 66 | ### 设置 IO 方向 67 | 68 | 服务端 -> 客户端 69 | 70 | 行为:设置 FPGA 上引脚的输入输出方向,每个位为 0 代表输入到实验 FPGA, 1 代表从实验 FPGA 输出。 71 | 72 | 格式: 73 | 74 | ```json 75 | {"SetIODirection":{"mask":"00111","data":"00000"}} 76 | ``` 77 | 78 | ### 设置 IO 输出 79 | 80 | 服务端 -> 客户端 81 | 82 | 行为:设置实验 FPGA 上引脚的输入值,0 代表低电平,1 代表高电平。 83 | 84 | 格式: 85 | 86 | ```json 87 | {"SetIOOutput":{"mask":"11100","data":"01000"}} 88 | ``` 89 | 90 | ### 订阅 IO 状态更新 91 | 92 | 服务端 -> 客户端 93 | 94 | 行为:订阅实验 FPGA 输出引脚上状态的更新。 95 | 96 | 格式: 97 | 98 | ```json 99 | {"SubscribeIOChange":""} 100 | ``` 101 | 102 | ### 停止订阅 IO 状态更新 103 | 104 | 服务端 -> 客户端 105 | 106 | 行为:停止订阅实验 FPGA 输出引脚上状态的更新。 107 | 108 | 格式: 109 | 110 | ```json 111 | {"UnsubscribeIOChange":""} 112 | ``` 113 | 114 | ### 设置并使能用户时钟 115 | 116 | 服务端 -> 客户端 117 | 118 | 行为:设置时钟频率并使能。 119 | 120 | 格式: 121 | 122 | ```json 123 | {"EnableUserClock":{"frequency":3000000}} 124 | ``` 125 | 126 | ### 关闭用户时钟 127 | 128 | 服务端 -> 客户端 129 | 130 | 行为:关闭时钟。 131 | 132 | 格式: 133 | 134 | ```json 135 | {"DisableUserClock":""} 136 | ``` 137 | 138 | ### 设置板子显示 139 | 140 | 服务端 -> 客户端 141 | 142 | 行为:打开/关闭板子显示。 143 | 144 | 格式: 145 | 146 | ```json 147 | {"Ident":true} 148 | ``` 149 | 150 | 151 | 152 | # 后端与前端通信协议 153 | 154 | 通过 WebSocket 通信,由前端发起,endpoint 为 /api/ws_user 。 155 | 156 | ## 请求格式 157 | 158 | ### 请求分配板子 159 | 160 | 前端 -> 后端 161 | 162 | 格式: 163 | 164 | ```json 165 | {"RequestForBoard":""} 166 | ``` 167 | 168 | ### 板子分配结果 169 | 170 | 后端 -> 前端 171 | 172 | 如果成功了,返回板子的一个唯一标识;如果失败了,返回 null 173 | 174 | 格式: 175 | 176 | ```json 177 | {"BoardAllocateResult":"1234"} 178 | ``` 179 | 180 | ### 发给板子的消息 181 | 182 | 前端 -> 后端 183 | 184 | 行为:必须先分配到板子。具体格式见上面对应的消息。 185 | 186 | 格式: 187 | 188 | ```json 189 | {"ToBoard":{"SetIODirection":{"mask":"11111","data":"10101"}}} 190 | ``` 191 | 192 | ### 从板子接收消息 193 | 194 | 后端 -> 前端 195 | 196 | 行为:必须先分配到板子。具体格式见上面对应的消息。 197 | 198 | 格式: 199 | 200 | ```json 201 | {"ReportIOChange":{"data":"0101010101"}} 202 | ``` 203 | 204 | ### 板子断开 205 | 206 | 后端 -> 前端 207 | 208 | 行为:必须先分配到板子。后端通知前端,目前分配的板子断开了连接。 209 | 210 | 格式: 211 | 212 | ```json 213 | {"BoardDisconnected":""} 214 | ``` 215 | 216 | ### Bitstream 编程 217 | 218 | 前端 -> 后端 219 | 220 | 行为:必须先分配到板子。对分配的板子烧入某已完成构建的 bitstream。 221 | 222 | 格式: 223 | 224 | ```json 225 | {"ProgramBitstream":1234} 226 | ``` 227 | 228 | ### Bitstream 编程结果 229 | 230 | 后端 -> 前端 231 | 232 | 行为:在向 FPGA 编程 Bitstream后,汇报编程结果。 233 | 234 | 格式: 235 | 236 | ```json 237 | {"ProgramBitstreamFinish":true} 238 | ``` 239 | 240 | -------------------------------------------------------------------------------- /uninstaller/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('activate', e => { 2 | caches.keys().then(names => { 3 | for(const name of names) caches.delete(name); 4 | return self.registration.unregister(); 5 | }).then(() => self.clients.matchAll()).then(clients => { 6 | for(const client of clients) client.navigate(client.url); 7 | }); 8 | }) 9 | 10 | self.addEventListener('message', (event) => { 11 | if (event.data && event.data.type === 'SKIP_WAITING') { 12 | self.skipWaiting(); 13 | } 14 | }); 15 | 16 | --------------------------------------------------------------------------------