Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3400 打包过长的日志会造成 java.lang.OutOfMemoryError #3468

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 13 additions & 4 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -94,8 +94,17 @@ private static void processLogs(Path directory, String fileExtension, String log
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
if (time.toMillis() >= processStartTime) {
try {
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
zipper.putTextFile(crashLog, file.getFileName().toString());
if (Files.size(file) >= 1024 * 1024 * 5) {
try (
Reader reader = Files.newBufferedReader(file, OperatingSystem.NATIVE_CHARSET);
Writer writer = new BufferedWriter(new OutputStreamWriter(zipper.putStream(file.getFileName().toString()), StandardCharsets.UTF_8))
) {
Logger.filterForbiddenToken(reader, writer);
}
} else {
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
zipper.putTextFile(crashLog, file.getFileName().toString());
}
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
}
Expand Down
44 changes: 40 additions & 4 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Zipper.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@
package org.jackhuang.hmcl.util.io;

import org.jackhuang.hmcl.util.function.ExceptionalPredicate;
import org.jetbrains.annotations.NotNull;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
Expand Down Expand Up @@ -149,6 +147,44 @@ public void putFile(Path file, String path) throws IOException {
}
}

public OutputStream putStream(String path) throws IOException {
zos.putNextEntry(new ZipEntry(normalize(path)));

return new OutputStream() {
private volatile boolean closed;

private void ensureOpen() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
}

@Override
public void write(byte @NotNull [] b) throws IOException {
ensureOpen();
zos.write(b);
}

@Override
public void write(byte @NotNull [] b, int off, int len) throws IOException {
ensureOpen();
zos.write(b, off, len);
}

@Override
public void close() throws IOException {
closed = true;
zos.closeEntry();
}

@Override
public void write(int b) throws IOException {
ensureOpen();
zos.write(b);
}
};
}

public void putStream(InputStream in, String path) throws IOException {
zos.putNextEntry(new ZipEntry(normalize(path)));
IOUtils.copyTo(in, zos, buffer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import org.tukaani.xz.XZOutputStream;

import java.io.*;
import java.nio.file.*;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
Expand All @@ -25,7 +28,7 @@
public final class Logger {
public static final Logger LOG = new Logger();

private static volatile String[] accessTokens = new String[0];
static volatile String[] accessTokens = new String[0];

public static synchronized void registerAccessToken(String token) {
final String[] oldAccessTokens = accessTokens;
Expand All @@ -37,11 +40,12 @@ public static synchronized void registerAccessToken(String token) {
}

public static String filterForbiddenToken(String message) {
for (String token : accessTokens)
message = message.replace(token, "<access token>");
return message;
return TokenFence.filter(accessTokens, message);
}

public static void filterForbiddenToken(Reader reader, Writer out) throws IOException {
TokenFence.filter(accessTokens, reader, out);
}

static final String CLASS_NAME = Logger.class.getName();

Expand Down
127 changes: 127 additions & 0 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/util/logging/TokenFence.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.jackhuang.hmcl.util.logging;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

final class TokenFence {
private TokenFence() {
}

public static String filter(String[] accessTokens, String message) {
for (String token : accessTokens)
message = message.replace(token, "<access token>");
return message;
}

public static void filter(String[] accessTokens, Reader reader, Writer out) throws IOException {
char[] buffer = new char[Math.max(1024, accessTokens[0].length() + 16)];
if (accessTokens.length == 1) {
filter(accessTokens[0], reader, out, buffer);
} else {
Path t1 = Files.createTempFile("hmcl-token-fence-", ".txt"), t2 = Files.createTempFile("hmcl-token-fence-", ".txt");
try (Writer o1 = Files.newBufferedWriter(t1, StandardCharsets.UTF_8)) {
filter(accessTokens[0], reader, o1, buffer);
}

for (int i = 1; i < accessTokens.length - 1; i++) {
String token = accessTokens[i];
if (token.length() > buffer.length) {
buffer = new char[token.length() + 16];
}

try (Reader i1 = Files.newBufferedReader(t1, StandardCharsets.UTF_8); Writer i2 = Files.newBufferedWriter(t2, StandardCharsets.UTF_8)) {
filter(token, i1, i2, buffer);
}

Path t3 = t2;
t2 = t1;
t1 = t3;
}

try (Reader r1 = Files.newBufferedReader(t1, StandardCharsets.UTF_8)) {
filter(accessTokens[accessTokens.length - 1], r1, out, buffer);
}

Files.delete(t1);
Files.delete(t2);
}

for (String token : accessTokens) {
if (token.length() > buffer.length) {
buffer = new char[token.length() + 16];
}
filter(token, reader, out, buffer);
}
}

private static void filter(String token, Reader reader, Writer out, char[] buffer) throws IOException {
char first = token.charAt(0);
int start = 0, length;
while ((length = reader.read(buffer, start, buffer.length - start)) != -1 || start != 0) {
if (start != 0) {
length = (length == -1 ? 0 : length) + start;
}
if (length < token.length()) {
out.write(buffer, 0, length);
return;
}

int tail = length - token.length() + 1, fi = findToken(buffer, tail, token);
if (fi == -1) {
int fi2 = indexOf(buffer, tail, length, first);
if (fi2 == -1) {
out.write(buffer, 0, length);
start = 0;
} else {
out.write(buffer, 0, fi2);
System.arraycopy(buffer, fi2, buffer, 0, length - fi2);
start = length - fi2;
}
} else {
out.write(buffer, 0, fi);
out.write("<access token>");
start = length - fi - token.length();
System.arraycopy(buffer, fi + token.length(), buffer, 0, start);
}
}
}

private static int findToken(char[] buffer, int tail, String token) {
char first = token.charAt(0);
int start = 0;
while (true) {
int fi = indexOf(buffer, start, tail, first);
if (fi == -1) {
return -1;
}
if (isToken(buffer, fi, token, token.length())) {
return fi;
}
start = fi + 1;
}
}

private static int indexOf(char[] buffer, int start, int length, char target) {
for (int i = start; i < length; i++) {
if (buffer[i] == target) {
return i;
}
}

return -1;
}

private static boolean isToken(char[] buffer, int start, String token, int length) {
for (int i = 1; i < token.length(); i++) {
if (buffer[start + i] != token.charAt(i)) {
return false;
}
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.logger;

import org.jackhuang.hmcl.util.logging.Logger;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class LoggerTest {
@Test
public void checkTokenFence() throws IOException {
for (String s : new String[]{
"a_token", "the_token", "another_token"
}) {
Logger.registerAccessToken(s);
}

test("a_token001122334455the_token667788another_token");

{
char[] data = new char[1050];
"the_token".getChars(0, "the_token".length(), data, 1020);
"another_token".getChars(0, "another_token".length(), data, 1035);
test(data);
}
}

private void test(char[] data) throws IOException {
test(new String(data));
}

private void test(String data) throws IOException {
try (StringWriter writer = new StringWriter()) {
Logger.filterForbiddenToken(new StringReader(data), writer);

assertEquals(Logger.filterForbiddenToken(data), writer.toString());
}
}
}