Skip to content

Start JDT LS with a UTF-8 locale when the environment selects ASCII - #4473

Open
wenytang-ms wants to merge 1 commit into
redhat-developer:mainfrom
wenytang-ms:fix/jdtls-non-ascii-file-names
Open

Start JDT LS with a UTF-8 locale when the environment selects ASCII#4473
wenytang-ms wants to merge 1 commit into
redhat-developer:mainfrom
wenytang-ms:fix/jdtls-non-ascii-file-names

Conversation

@wenytang-ms

Copy link
Copy Markdown
Contributor

Fixes microsoft/vscode-java-dependency#1041

Problem

Inside containers, importing a workspace that contains a file with a non-ASCII name aborts with:

!MESSAGE Progressive import: reporting 1 new project(s)
!MESSAGE Initialization failed
java.nio.file.InvalidPathException: Malformed input or input contains unmappable characters:
  /workspace/JAVA_Projects/RuoYi/doc/????????????????????????.docx

Every project after the failing one silently disappears. The reporter saw a 7-module Maven project collapse to a single root project, which looks exactly like a multi-module import bug — the encoding cause is invisible from the UI. The sample is yangzongzhuan/RuoYi, whose doc/ folder contains 若依环境使用手册.docx: 8 CJK characters = 24 UTF-8 bytes, matching the 24 replacement characters in the log exactly (one per byte).

It reproduces only in containers. The same workspace works locally and over SSH remote, because those sessions have a UTF-8 locale.

Root cause

On POSIX systems the JVM decodes and encodes file names with sun.jnu.encoding, which it derives from the process locale at startup (OpenJDK jdk-21+35):

Containers commonly start with an unset or C/POSIX locale, so sun.jnu.encoding becomes ANSI_X3.4-1968.

Crucially, -Dsun.jnu.encoding and -Dfile.encoding cannot fix this — the property is written from the OS locale during VM startup and the command-line value is ignored. Verified on JDK 21: java -Dsun.jnu.encoding=US-ASCII still reports the locale-derived charset. So java.jdt.ls.vmargs gives users no way out; the process locale is the only lever, and this extension is the only component in a position to set it.

Change

prepareExecutable now merges getUnicodeLocaleEnv() into the JDT LS process environment. It corrects the locale when, and only when, the inherited one cannot represent non-ASCII file names:

  • Reads LC_ALL > LC_CTYPE > LANG, the precedence POSIX defines for LC_CTYPE.
  • Overrides only the unset / C / POSIX / C.ASCII cases. A deliberately configured non-UTF-8 locale such as zh_CN.GBK is left untouched, since file names on such a system may genuinely be stored in that encoding.
  • Sets LC_ALL=C.UTF-8 only when LC_ALL is the variable selecting ASCII (it dominates every category); otherwise sets just LC_CTYPE=C.UTF-8, so message, number and time formatting keep following the environment.
  • Logs one line to the Java output channel when it acts, so the situation stays diagnosable.

Why Windows returns early

Windows is structurally unaffected, and does not read these variables at all:

Setting POSIX locale variables there would be a no-op for the JVM while still leaking into every process JDT LS spawns (Maven/Gradle wrappers, Git for Windows, MSYS-based tooling do read them).

macOS is also unaffected — sun_jnu_encoding is hard-coded to UTF-8 there (java_props_md.c#L451-L452) — and the ASCII check makes this a no-op in practice anyway.

Known limitation

C.UTF-8 requires glibc >= 2.35 or a distro that ships it (Debian and Ubuntu do; RHEL 7 does not). Where it is missing, setlocale fails and the JVM falls back to C, i.e. current behaviour — no regression, but no fix either. Those users still need en_US.UTF-8 generated in the image.

Non-VS Code clients that launch JDT LS through jdtls.py are unaffected by this change; a matching fix can be made in eclipse.jdt.ls separately.

Tests

test/standard-mode-suite/javaServerStarter.test.ts covers the decision table: Windows early return, unset locale, C, POSIX, C.ASCII, en_US.UTF-8, zh_CN.GBK, LC_ALL override, LC_CTYPE-only correction, and LC_CTYPE precedence over LANG.

The JVM derives sun.jnu.encoding - the charset it uses to decode and encode file
names on POSIX systems - from the process locale at startup, and silently
ignores -Dsun.jnu.encoding, so neither java.jdt.ls.vmargs nor -Dfile.encoding
can influence it.

Containers routinely start with an unset or C/POSIX locale, which makes the JVM
fall back to ASCII. Any workspace file whose name contains non-ASCII characters
is then decoded into U+FFFD and can no longer be turned into a
java.nio.file.Path, so importing the workspace fails with
"InvalidPathException: Malformed input or input contains unmappable characters"
and every project after the failing one is missing from the Java Projects view.
This is why the reported project only reproduces inside a container and works
fine locally and over SSH remote.

Correct the locale of the JDT LS process when, and only when, the inherited one
cannot represent non-ASCII file names. A deliberately configured non-UTF-8
locale such as zh_CN.GBK is left untouched, and only the LC_CTYPE category is
set unless LC_ALL is the variable selecting ASCII, so message, number and time
formatting keep following the environment.

Fixes microsoft/vscode-java-dependency#1041
@wenytang-ms

Copy link
Copy Markdown
Contributor Author

Verification

Verified end-to-end on Linux against the reported sample: real JDT LS (jdt-language-server-latest), real yangzongzhuan/RuoYi (7 Maven modules, contains doc/若依环境使用手册.docx), real Maven import, driven headlessly over stdio LSP. The locale is the only variable between the two runs.

BeforeLANG=C AfterLANG=C LC_CTYPE=C.UTF-8
Projects imported 1 (ruoyi only) 7 (all modules)
Fatal errors in .metadata/.log 2 0
language/status Error: Malformed input… ProjectStatus: OK

Before, reproducing the reported failure line for line:

Importing Maven project(s)
Progressive import: reporting 1 new project(s)
Initialization failed
[status/Error] Malformed input or input contains unmappable characters:
               .../RuoYi/doc/������������������������.docx

Imported Eclipse projects: 1
   - ruoyi

After, with the value this PR injects:

[status/ProjectStatus] OK

Imported Eclipse projects: 7
   - ruoyi, ruoyi-admin, ruoyi-common, ruoyi-framework,
     ruoyi-generator, ruoyi-quartz, ruoyi-system

Fatal errors in .metadata/.log: 0

At the JVM level, each row run directly against that file name (8 CJK chars = 24 UTF-8 bytes), on Temurin 21.0.12 / glibc 2.35:

Environment sun.jnu.encoding U+FFFD in name Paths.get()
LANG=C ANSI_X3.4-1968 24 InvalidPathException
all unset ANSI_X3.4-1968 24 InvalidPathException
LC_ALL=C + LC_CTYPE=C.UTF-8 ANSI_X3.4-1968 24 InvalidPathException
LC_ALL=C.UTF-8 UTF-8 0 OK
LANG=C + LC_CTYPE=C.UTF-8 UTF-8 0 OK

Row 3 is why LC_ALL is replaced when it is the variable selecting ASCII, instead of always setting LC_CTYPE; rows 4-5 are the two branches this PR takes.

One note for reviewers: this is not a narrow edge case. JDT LS ships only the pure-Java org.eclipse.core.filesystem bundle with no native fragment, so LocalFileNativesManager always falls through to the PosixHandler branch on Linux — the exact frame in the reported stack trace.

@wenytang-ms

Copy link
Copy Markdown
Contributor Author

@datho7561 please help review this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cannot recognize Maven projects with multiple submodules

1 participant