Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ name: Release
#
# No step receives a signing key. Checkouts do not keep the workflow token.
#
# Releases stay pre-releases, which Lithe Store's tracker skips, for two
# Releases stay pre-releases, which Lithe Store's tracker skips, for three
# reasons:
# 1. Lithe still bundles Java Support under the old ID
# dev.lithe.plugin.java-support.
# 2. Current Lithe hosts reserve languageServers[].hostIntegration (the JDT
# integration) to dev.lithe.* plugin IDs and accept the short tool ID
# "java" only from verified official provenance, so they reject this
# package.
# 3. The backend links Rust plugin SDK 0.2.0, which requires the host to
# offer litheLogging 1 and refuses to start without it. Only the latest
# Lithe preview offers it so far; no stable Lithe does.
# Remove --prerelease below once a stable Lithe accepts the package (stage E in
# MIGRATION.md) and stops bundling Java Support. The Store then lists stable
# releases of this repository automatically.
# MIGRATION.md), stops bundling Java Support, and offers litheLogging 1 (Rust
# SDK 0.2.0 requires it). The Store then lists stable releases of this
# repository automatically.
on:
push:
tags: ["v*"]
Expand Down
11 changes: 9 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 50 additions & 37 deletions MIGRATION.md

Large diffs are not rendered by default.

29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
## 工作方式

- **领域规则**在 `crates/java-domain`,不依赖宿主:命令的 JSON 载荷和错误码与主仓库 Core 一一对应。
所有宿主差异(存储、日志、Git、语言服务器通道)都是带 `TODO(host-api: API-NN)` 的占位。
所有宿主差异(存储、Git、语言服务器通道)都是带 `TODO(host-api: API-NN)` 的占位;日志已接入宿主统一日志,见“日志”一节。
- **插件后端**在 `backend/`:基于固定版本的 Rust SDK(`sdk.lock.json`),把每条领域命令注册为同名插件函数,
并带着跨插件函数的调用方一侧。
- **跨插件函数**:启动 JDT LS 之前,后端通过 Core 中转发现所有导出 `languageImport.settings` 的插件(Maven、Gradle),
Expand All @@ -41,6 +41,25 @@
- **运行时只读**:插件包安装后在运行时只读。JDT LS、Lombok、java-debug、测试运行器和 JDK 都在构建阶段固定来源、校验哈希后放进包里,
运行时状态只写宿主提供的状态目录,不写包目录。

## 日志

后端把日志交给宿主的统一日志 `litheLogging` 1,不写 stderr。函数 ID 都是编译期常量,注册只会因 ID 写错、重复或超出 SDK
上限而失败,单元测试 `the_backend_builds_with_every_command_registered` 在 CI 里挡住这种构建;万一发生,进程在握手前以状态 1 退出,
由宿主记录启动失败(`backend/src/main.rs`)。Rust SDK 0.2.0 要求宿主提供 `litheLogging` 1,
不提供的 Lithe 版本会拒绝启动后端;目前只有 Lithe 最新 `preview` 提供,稳定版还没有。

| 事实 | 级别 | 来源 | 内容 |
| --- | --- | --- | --- |
| 关键操作成功:`runConfig.selectJava`、`debug.javaTestLaunch`、`execution.planLaunchCommand`、`java.prepareLaunch`(`backend/src/lib.rs` 的 `KEY_OPERATIONS`) | INFO | SDK 自动 | 操作 ID、耗时 |
| 其余函数成功(编辑器驱动或启动时的读取,如 `java.runMarkers`、`java.codeVision`、`spring.index`) | 不记录 | `suppress_success_diagnostics`(`backend/src/lib.rs`) | — |
| 任一函数失败 | ERROR | SDK 自动 | 类别为 `pluginErrorCode`(如 `java.invalidRequest`)、耗时;不含消息与 `details` |
| 取消 | 不记录 | SDK | — |
| 列出 `languageImport.settings` 提供方失败、单个提供方失败或超时、答复不可用、答复含越界键 | WARN(`degraded`) | `backend/src/plugin_functions.rs` | 类别 `relay.providers_unavailable`、`relay.provider_failed`、`relay.provider_timed_out`、`relay.settings_rejected`、`relay.keys_rejected`,计数;不含提供方 ID |
| 插件存储清理失败 | WARN(`degraded`) | `crates/java-domain/src/host/diagnostics.rs`(`host::diagnostics`) | 类别 `storage.cleanup_failed.<阶段>`(阶段为 `jdt-configuration`、`jdt-settings`、`java-argfile-remove`),计数 |
| JDT LS 自身日志 | 上游,默认 WARNING | `plugin.json` 的 `-Dosgi.logfile=${logDir}/jdt.log` | 只写宿主提供的会话日志目录,单文件 1 MB、保留 2 个备份;用户可在设置里调 `logLevel` |

日志事实只带类别、计数和耗时,不含路径、源码、工具输出或用户内容。`${logDir}` 由宿主经可写存储适配器提供,不写插件包或 app bundle。

## 安装

Lithe 不再区分官方插件和普通插件,所有插件都按普通插件处理:本插件不带发布者签名(`plugin.json` 的 `vendor.signatureRequirement` 为 `none`)。
Expand Down Expand Up @@ -103,9 +122,11 @@ scripts/archive-package.sh --package <上一步打印的目录> --arch arm64 #

3. `Release` 工作流先在 Ubuntu 上跑 Rust 检查,再在 macOS 上构建、校验并归档 arm64 与 x86_64 两个包,然后发布。
发布资产是 `Lithe-Java-Support-<版本>-darwin-{arm64,x64}.zip` 和对应的 `.sha256`。没有任何步骤读取签名密钥,包也不带发布者签名。
4. 发布保持为预发布(`--prerelease`):Lithe Store 的自动跟踪会跳过预发布,而目前的 Lithe 一是仍然内置旧 ID 的 Java Support,
二是只允许 `dev.lithe.*` 插件使用 JDT 宿主集成、只对带官方来源的包接受短工具 ID `java`。等 Lithe 主仓库的阶段 E 完成
(`MIGRATION.md` 第 8.2 节)后,去掉 `--prerelease`,商店就会自动上架稳定版。细节见 `MIGRATION.md` 第 9 节。
4. 发布保持为预发布(`--prerelease`):Lithe Store 的自动跟踪会跳过预发布,原因有三个。目前的 Lithe 一是仍然内置旧 ID 的 Java Support,
二是只允许 `dev.lithe.*` 插件使用 JDT 宿主集成、只对带官方来源的包接受短工具 ID `java`,
三是后端链接的 Rust SDK 0.2.0 需要宿主提供 `litheLogging` 1,目前只有 Lithe 最新 `preview` 提供,稳定版还没有。
等 Lithe 主仓库的阶段 E 完成(`MIGRATION.md` 第 8.2 节)、并且 Lithe 稳定版提供 `litheLogging` 1 后,去掉 `--prerelease`,
商店就会自动上架稳定版。细节见 `MIGRATION.md` 第 9 节。

## 许可

Expand Down
2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ lithe-java-domain = { path = "../crates/java-domain" }
# Pinned to the published Rust SDK release; ../sdk.lock.json records the
# matching commit, crate archive and SHA-256; scripts/verify-sdk-lock.sh
# checks that Cargo.lock resolved this exact commit.
lithe-plugin-sdk = { git = "https://github.com/Lithe-IDEA/lithe-plugin-docs", tag = "sdk-rust-v0.1.0" }
lithe-plugin-sdk = { git = "https://github.com/Lithe-IDEA/lithe-plugin-docs", tag = "sdk-rust-v0.2.0" }
serde_json.workspace = true
110 changes: 90 additions & 20 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,52 @@
//! `languageImport.settings` (API-35): it asks the Maven and Gradle plugins
//! for the settings JDT LS imports with. It is the only module that knows the
//! host relay's wire.
//!
//! # Logging
//!
//! The backend reports restricted facts through the SDK's `litheLogging` 1
//! channel; the SDK refuses `initialize` when the host does not offer it. The
//! SDK reports each function's terminal fact by itself: a failure as
//! `operationFailed` with the `pluginErrorCode`, a success as
//! `operationFinished` except for the functions outside [`KEY_OPERATIONS`],
//! whose success is suppressed. Failures the domain layer handled and
//! recovered from are reported as `degraded` through
//! `lithe_java_domain::host::diagnostics`. The backend never writes to
//! stderr, and a fact carries no path, source or tool output.

pub mod plugin_functions;

use lithe_java_domain::commands::{self, ExecuteOptions, SESSION_COMMANDS, SUPPORTED_COMMANDS};
use lithe_java_domain::host::diagnostics::{scoped, Reporter};
use lithe_java_domain::protocol::{CoreError, ErrorCode};
use lithe_plugin_sdk::{Backend, Context, FunctionError, PluginError, SdkError, Value};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;

/// Name reported as `serverInfo.name`.
pub const BACKEND_NAME: &str = "lithe-java-backend";

/// Functions whose success is a key user operation and is logged at INFO.
/// Every other function is an editor-driven or startup read; its success is
/// suppressed, and its failures are still logged.
pub const KEY_OPERATIONS: &[&str] = &[
"debug.javaTestLaunch",
"execution.planLaunchCommand",
"java.prepareLaunch",
"runConfig.selectJava",
];

/// Builds the backend with every Java Support function registered.
///
/// A function can only be suppressed once it is registered, so the successes
/// of the functions outside [`KEY_OPERATIONS`] are suppressed after the
/// registration loop.
///
/// Every input is a compile-time constant, so the result is the same on each
/// run: it fails only when a function ID is invalid, repeated or over the
/// SDK's limit, which the test `the_backend_builds_with_every_command_registered`
/// rejects.
pub fn backend() -> Result<Backend, SdkError> {
let mut backend = Backend::new(BACKEND_NAME).version(env!("CARGO_PKG_VERSION"));
for command in SUPPORTED_COMMANDS.iter().chain(SESSION_COMMANDS) {
Expand All @@ -38,6 +71,11 @@ pub fn backend() -> Result<Backend, SdkError> {
invoke(command, params, context)
})?;
}
for command in SUPPORTED_COMMANDS.iter().chain(SESSION_COMMANDS) {
if !KEY_OPERATIONS.contains(command) {
backend.suppress_success_diagnostics(command)?;
}
}
Ok(backend)
}

Expand All @@ -62,34 +100,41 @@ fn invoke(command: &str, params: Value, context: &Context) -> Result<Value, Func
commands::cancel(&operation_id);
}
});
let result = commands::execute_with_options(
command,
params,
ExecuteOptions {
operation_id: Some(operation_id),
timeout_milliseconds: None,
},
);
// The domain layer reports failures it handled itself (for example a
// storage cleanup that failed) to this thread's reporter. The category is
// controlled text and the count is a number; an error from `degraded` only
// means the arguments were invalid, so the call's own result stands.
let diagnostics = context.diagnostics();
let operation = context.function_id().to_owned();
let reporter: Reporter = Rc::new(move |code: &str, count: u64| {
let _ = diagnostics.degraded(&operation, code, Some(count));
});
let result = scoped(reporter, || {
commands::execute_with_options(
command,
params,
ExecuteOptions {
operation_id: Some(operation_id),
timeout_milliseconds: None,
},
)
});
if cancelled.load(Ordering::SeqCst) {
return Err(FunctionError::Cancelled);
}
result.map_err(|error| function_error(error, context))
result.map_err(function_error)
}

/// Maps a domain failure to the error the host receives.
///
/// `details` may carry paths or tool output, so it only reaches stderr.
fn function_error(error: CoreError, context: &Context) -> FunctionError {
/// The SDK reports the failure once, at the function's result boundary, with
/// the `pluginErrorCode` as its category, so this function logs nothing.
/// `details` may carry paths or tool output and never leaves the backend: it
/// is neither part of the error nor part of any log fact.
fn function_error(error: CoreError) -> FunctionError {
if error.code == ErrorCode::Cancelled {
return FunctionError::Cancelled;
}
if let Some(details) = &error.details {
context.log(format!(
"{} failed: {}: {details}",
context.function_id(),
error.message
));
}
// TODO(host-api: API-37 i18n): the messages are English Core messages.
// Localize them through the host i18n service once plugin backends can
// reach it.
Expand Down Expand Up @@ -142,11 +187,36 @@ mod tests {
.all(|code| lithe_plugin_sdk::is_plugin_error_code(code)));
}

/// `main` exits without a message when [`backend`] fails, so this test is
/// what keeps that branch out of every build that passed CI.
#[test]
fn every_command_is_a_valid_function_id() {
fn the_backend_builds_with_every_command_registered() {
for command in SUPPORTED_COMMANDS.iter().chain(SESSION_COMMANDS) {
assert!(lithe_plugin_sdk::is_function_id(command), "{command}");
}
assert!(backend().is_ok());
if let Err(error) = backend() {
panic!("the backend does not build: {}", error.code());
}
}

#[test]
fn key_operations_are_registered_commands_without_duplicates() {
let commands: std::collections::BTreeSet<&str> = SUPPORTED_COMMANDS
.iter()
.chain(SESSION_COMMANDS)
.copied()
.collect();
let distinct: std::collections::BTreeSet<&str> = KEY_OPERATIONS.iter().copied().collect();
assert_eq!(
distinct.len(),
KEY_OPERATIONS.len(),
"a key operation is listed twice"
);
for operation in KEY_OPERATIONS {
assert!(
commands.contains(operation),
"{operation} is not a registered command"
);
}
}
}
20 changes: 13 additions & 7 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
//! Entry point of the Java Support plugin backend.
//!
//! Stdout carries only protocol frames; diagnostics go to stderr.
//! Stdout carries only protocol frames, and the backend writes nothing to
//! stderr: diagnostics go through the host's `litheLogging` channel.
//!
//! Building the backend only fills the SDK's in-memory function table; no
//! message is sent until the host's `initialize`. It fails only when a
//! function ID constant is invalid, repeated or over the SDK's limit, which
//! is fixed at compile time and rejected by the unit test
//! `the_backend_builds_with_every_command_registered`. A build that passed
//! the tests therefore never takes the error branch; if one did, the process
//! exits with status 1 before the handshake and the host records the failed
//! start itself.

fn main() {
let backend = match lithe_java_backend::backend() {
Ok(backend) => backend,
Err(error) => {
eprintln!("lithe-java-backend: could not register functions: {error}");
std::process::exit(1);
}
let Ok(backend) = lithe_java_backend::backend() else {
std::process::exit(1);
};
std::process::exit(backend.listen());
}
Loading
Loading