From 4b7204c9607b1edf641a2b7e378d01f157c06105 Mon Sep 17 00:00:00 2001 From: fit2-zhao Date: Wed, 28 Apr 2021 19:53:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=8E=A5=E5=8F=A3=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96):=20=E6=B5=8B=E8=AF=95=E8=AE=A1=E5=88=92=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=89=A7=E8=A1=8C=E5=9C=BA=E6=99=AF=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=EF=BC=8C=E5=BE=AA=E7=8E=AF=E6=8E=A7=E5=88=B6=E5=99=A8=E5=8F=AA?= =?UTF-8?q?=E8=83=BD=E8=AF=BB=E5=8F=96=E5=88=B0=E4=B8=80=E4=B8=AA=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/definition/request/MsTestElement.java | 2 +- .../apache/jmeter/services/FileServer.java | 582 ++++++++++++++++++ frontend/src/business/components/xpack | 2 +- 3 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/org/apache/jmeter/services/FileServer.java diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java index a34f222688..510e4e3395 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java @@ -206,7 +206,7 @@ public abstract class MsTestElement { } csvDataSet.setIgnoreFirstLine(false); csvDataSet.setRecycle(true); - csvDataSet.setProperty("shareMode","shareMode.thread"); + //csvDataSet.setProperty("shareMode","shareMode.group"); csvDataSet.setProperty("recycle", true); csvDataSet.setProperty("delimiter", item.getDelimiter()); csvDataSet.setComment(StringUtils.isEmpty(item.getDescription()) ? "" : item.getDescription()); diff --git a/backend/src/main/java/org/apache/jmeter/services/FileServer.java b/backend/src/main/java/org/apache/jmeter/services/FileServer.java new file mode 100644 index 0000000000..d51e9a836b --- /dev/null +++ b/backend/src/main/java/org/apache/jmeter/services/FileServer.java @@ -0,0 +1,582 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jmeter.services; + +import org.apache.commons.io.input.BOMInputStream; +import org.apache.jmeter.gui.JMeterFileFilter; +import org.apache.jmeter.save.CSVSaveService; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.util.JOrphanUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.file.Files; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * This class provides thread-safe access to files, and to + * provide some simplifying assumptions about where to find files and how to + * name them. For instance, putting supporting files in the same directory as + * the saved test plan file allows users to refer to the file with just it's + * name - this FileServer class will find the file without a problem. + * Eventually, I want all in-test file access to be done through here, with the + * goal of packaging up entire test plans as a directory structure that can be + * sent via rmi to remote servers (currently, one must make sure the remote + * server has all support files in a relative-same location) and to package up + * test plans to execute on unknown boxes that only have Java installed. + */ +public class FileServer { + + private static final Logger log = LoggerFactory.getLogger(FileServer.class); + + /** + * The default base used for resolving relative files, i.e.
+ * {@code System.getProperty("user.dir")} + */ + private static final String DEFAULT_BASE = System.getProperty("user.dir");// $NON-NLS-1$ + + /** Default base prefix: {@value} */ + private static final String BASE_PREFIX_DEFAULT = "~/"; // $NON-NLS-1$ + + private static final String BASE_PREFIX = + JMeterUtils.getPropDefault("jmeter.save.saveservice.base_prefix", // $NON-NLS-1$ + BASE_PREFIX_DEFAULT); + + private File base; + + private final Map files = new HashMap<>(); + + private static final FileServer server = new FileServer(); + + // volatile needed to ensure safe publication + private volatile String scriptName; + + // Cannot be instantiated + private FileServer() { + base = new File(DEFAULT_BASE); + log.info("Default base='{}'", DEFAULT_BASE); + } + + /** + * @return the singleton instance of the server. + */ + public static FileServer getFileServer() { + return server; + } + + /** + * Resets the current base to DEFAULT_BASE. + */ + public synchronized void resetBase() { + checkForOpenFiles(); + base = new File(DEFAULT_BASE); + log.info("Reset base to '{}'", base); + } + + /** + * Sets the current base directory for relative file names from the provided path. + * If the path does not refer to an existing directory, then its parent is used. + * Normally the provided path is a file, so using the parent directory is appropriate. + * + * @param basedir the path to set, or {@code null} if the GUI is being cleared + * @throws IllegalStateException if files are still open + */ + public synchronized void setBasedir(String basedir) { + checkForOpenFiles(); // TODO should this be called if basedir == null? + if (basedir != null) { + File newBase = new File(basedir); + if (!newBase.isDirectory()) { + newBase = newBase.getParentFile(); + } + base = newBase; + log.info("Set new base='{}'", base); + } + } + + /** + * Sets the current base directory for relative file names from the provided script file. + * The parameter is assumed to be the path to a JMX file, so the base directory is derived + * from its parent. + * + * @param scriptPath the path of the script file; must be not be {@code null} + * @throws IllegalStateException if files are still open + * @throws IllegalArgumentException if scriptPath parameter is null + */ + public synchronized void setBaseForScript(File scriptPath) { + if (scriptPath == null){ + throw new IllegalArgumentException("scriptPath must not be null"); + } + setScriptName(scriptPath.getName()); + // getParentFile() may not work on relative paths + setBase(scriptPath.getAbsoluteFile().getParentFile()); + } + + /** + * Sets the current base directory for relative file names. + * + * @param jmxBase the path of the script file base directory, cannot be null + * @throws IllegalStateException if files are still open + * @throws IllegalArgumentException if {@code basepath} is null + */ + public synchronized void setBase(File jmxBase) { + if (jmxBase == null) { + throw new IllegalArgumentException("jmxBase must not be null"); + } + checkForOpenFiles(); + base = jmxBase; + log.info("Set new base='{}'", base); + } + + /** + * Check if there are entries in use. + *

+ * Caller must ensure that access to the files map is single-threaded as + * there is a window between checking the files Map and clearing it. + * + * @throws IllegalStateException if there are any entries still in use + */ + private void checkForOpenFiles() throws IllegalStateException { + if (filesOpen()) { // checks for entries in use + throw new IllegalStateException("Files are still open, cannot change base directory"); + } + files.clear(); // tidy up any unused entries + } + + public synchronized String getBaseDir() { + return base.getAbsolutePath(); + } + + public static String getDefaultBase(){ + return DEFAULT_BASE; + } + + /** + * Calculates the relative path from DEFAULT_BASE to the current base, + * which must be the same as or a child of the default. + * + * @return the relative path, or {@code "."} if the path cannot be determined + */ + public synchronized File getBaseDirRelative() { + // Must first convert to absolute path names to ensure parents are available + File parent = new File(DEFAULT_BASE).getAbsoluteFile(); + File f = base.getAbsoluteFile(); + ArrayDeque l = new ArrayDeque<>(); + while (f != null) { + if (f.equals(parent)){ + if (l.isEmpty()){ + break; + } + File rel = new File(l.pop()); + while(!l.isEmpty()) { + rel = new File(rel, l.pop()); + } + return rel; + } + l.push(f.getName()); + f = f.getParentFile(); + } + return new File("."); + } + + /** + * Creates an association between a filename and a File inputOutputObject, + * and stores it for later use - unless it is already stored. + * + * @param filename - relative (to base) or absolute file name (must not be null) + */ + public void reserveFile(String filename) { + reserveFile(filename,null); + } + + /** + * Creates an association between a filename and a File inputOutputObject, + * and stores it for later use - unless it is already stored. + * + * @param filename - relative (to base) or absolute file name (must not be null) + * @param charsetName - the character set encoding to use for the file (may be null) + */ + public void reserveFile(String filename, String charsetName) { + reserveFile(filename, charsetName, filename, false); + } + + /** + * Creates an association between a filename and a File inputOutputObject, + * and stores it for later use - unless it is already stored. + * + * @param filename - relative (to base) or absolute file name (must not be null) + * @param charsetName - the character set encoding to use for the file (may be null) + * @param alias - the name to be used to access the object (must not be null) + */ + public void reserveFile(String filename, String charsetName, String alias) { + reserveFile(filename, charsetName, alias, false); + } + + /** + * Creates an association between a filename and a File inputOutputObject, + * and stores it for later use - unless it is already stored. + * + * @param filename - relative (to base) or absolute file name (must not be null or empty) + * @param charsetName - the character set encoding to use for the file (may be null) + * @param alias - the name to be used to access the object (must not be null) + * @param hasHeader true if the file has a header line describing the contents + * @return the header line; may be null + * @throws IllegalArgumentException if header could not be read or filename is null or empty + */ + public synchronized String reserveFile(String filename, String charsetName, String alias, boolean hasHeader) { + if (filename == null || filename.isEmpty()){ + throw new IllegalArgumentException("Filename must not be null or empty"); + } + if (alias == null){ + throw new IllegalArgumentException("Alias must not be null"); + } + FileEntry fileEntry = files.get(alias); + if (fileEntry == null) { + fileEntry = new FileEntry(resolveFileFromPath(filename), null, charsetName); + if (filename.equals(alias)){ + log.info("Stored: {}", filename); + } else { + log.info("Stored: {} Alias: {}", filename, alias); + } + files.put(alias, fileEntry); + if (hasHeader) { + try { + fileEntry.headerLine = readLine(alias, false); + if (fileEntry.headerLine == null) { + fileEntry.exception = new EOFException("File is empty: " + fileEntry.file); + } + } catch (IOException | IllegalArgumentException e) { + fileEntry.exception = e; + } + } + } + if (hasHeader && fileEntry.headerLine == null) { + throw new IllegalArgumentException("Could not read file header line for file " + filename, + fileEntry.exception); + } + return fileEntry.headerLine; + } + + /** + * Resolves file name into {@link File} instance. + * When filename is not absolute and not found from current working dir, + * it tries to find it under current base directory + * @param filename original file name + * @return {@link File} instance + */ + private File resolveFileFromPath(String filename) { + File f = new File(filename); + if (f.isAbsolute() || f.exists()) { + return f; + } else { + return new File(base, filename); + } + } + + /** + * Get the next line of the named file, recycle by default. + * + * @param filename the filename or alias that was used to reserve the file + * @return String containing the next line in the file + * @throws IOException when reading of the file fails, or the file was not reserved properly + */ + public String readLine(String filename) throws IOException { + return readLine(filename, true); + } + + /** + * Get the next line of the named file, first line is name to false + * + * @param filename the filename or alias that was used to reserve the file + * @param recycle - should file be restarted at EOF? + * @return String containing the next line in the file (null if EOF reached and not recycle) + * @throws IOException when reading of the file fails, or the file was not reserved properly + */ + public String readLine(String filename, boolean recycle) throws IOException { + return readLine(filename, recycle, false); + } + /** + * Get the next line of the named file + * + * @param filename the filename or alias that was used to reserve the file + * @param recycle - should file be restarted at EOF? + * @param ignoreFirstLine - Ignore first line + * @return String containing the next line in the file (null if EOF reached and not recycle) + * @throws IOException when reading of the file fails, or the file was not reserved properly + */ + public synchronized String readLine(String filename, boolean recycle, + boolean ignoreFirstLine) throws IOException { + FileEntry fileEntry = files.get(filename); + if(fileEntry == null ){ + this.reserveFile(filename); + fileEntry = files.get(filename); + } + if (fileEntry != null) { + if (fileEntry.inputOutputObject == null) { + fileEntry.inputOutputObject = createBufferedReader(fileEntry); + } else if (!(fileEntry.inputOutputObject instanceof Reader)) { + throw new IOException("File " + filename + " already in use"); + } + BufferedReader reader = (BufferedReader) fileEntry.inputOutputObject; + String line = reader.readLine(); + if (line == null && recycle) { + reader.close(); + reader = createBufferedReader(fileEntry); + fileEntry.inputOutputObject = reader; + if (ignoreFirstLine) { + // read first line and forget + reader.readLine();//NOSONAR + } + line = reader.readLine(); + } + log.debug("Read:{}", line); + return line; + } + throw new IOException("File never reserved: "+filename); + } + + /** + * + * @param alias the file name or alias + * @param recycle whether the file should be re-started on EOF + * @param ignoreFirstLine whether the file contains a file header which will be ignored + * @param delim the delimiter to use for parsing + * @return the parsed line, will be empty if the file is at EOF + * @throws IOException when reading of the aliased file fails, or the file was not reserved properly + */ + public synchronized String[] getParsedLine(String alias, boolean recycle, boolean ignoreFirstLine, char delim) throws IOException { + BufferedReader reader = getReader(alias, recycle, ignoreFirstLine); + return CSVSaveService.csvReadFile(reader, delim); + } + + /** + * Return BufferedReader handling close if EOF reached and recycle is true + * and ignoring first line if ignoreFirstLine is true + * + * @param alias String alias + * @param recycle Recycle at eof + * @param ignoreFirstLine Ignore first line + * @return {@link BufferedReader} + */ + private BufferedReader getReader(String alias, boolean recycle, boolean ignoreFirstLine) throws IOException { + FileEntry fileEntry = files.get(alias); + if (fileEntry != null) { + BufferedReader reader; + if (fileEntry.inputOutputObject == null) { + reader = createBufferedReader(fileEntry); + fileEntry.inputOutputObject = reader; + if (ignoreFirstLine) { + // read first line and forget + reader.readLine(); //NOSONAR + } + } else if (!(fileEntry.inputOutputObject instanceof Reader)) { + throw new IOException("File " + alias + " already in use"); + } else { + reader = (BufferedReader) fileEntry.inputOutputObject; + if (recycle) { // need to check if we are at EOF already + reader.mark(1); + int peek = reader.read(); + if (peek == -1) { // already at EOF + reader.close(); + reader = createBufferedReader(fileEntry); + fileEntry.inputOutputObject = reader; + if (ignoreFirstLine) { + // read first line and forget + reader.readLine(); //NOSONAR + } + } else { // OK, we still have some data, restore it + reader.reset(); + } + } + } + return reader; + } else { + throw new IOException("File never reserved: "+alias); + } + } + + private BufferedReader createBufferedReader(FileEntry fileEntry) throws IOException { + if (!fileEntry.file.canRead() || !fileEntry.file.isFile()) { + throw new IllegalArgumentException("File "+ fileEntry.file.getName()+ " must exist and be readable"); + } + BOMInputStream fis = new BOMInputStream(Files.newInputStream(fileEntry.file.toPath())); //NOSONAR + InputStreamReader isr = null; + // If file encoding is specified, read using that encoding, otherwise use default platform encoding + String charsetName = fileEntry.charSetEncoding; + if(!JOrphanUtils.isBlank(charsetName)) { + isr = new InputStreamReader(fis, charsetName); + } else if (fis.hasBOM()) { + isr = new InputStreamReader(fis, fis.getBOM().getCharsetName()); + } else { + @SuppressWarnings("DefaultCharset") + final InputStreamReader withPlatformEncoding = new InputStreamReader(fis); + isr = withPlatformEncoding; + } + return new BufferedReader(isr); + } + + public synchronized void write(String filename, String value) throws IOException { + FileEntry fileEntry = files.get(filename); + if (fileEntry != null) { + if (fileEntry.inputOutputObject == null) { + fileEntry.inputOutputObject = createBufferedWriter(fileEntry); + } else if (!(fileEntry.inputOutputObject instanceof Writer)) { + throw new IOException("File " + filename + " already in use"); + } + BufferedWriter writer = (BufferedWriter) fileEntry.inputOutputObject; + log.debug("Write:{}", value); + writer.write(value); + } else { + throw new IOException("File never reserved: "+filename); + } + } + + private BufferedWriter createBufferedWriter(FileEntry fileEntry) throws IOException { + OutputStream fos = Files.newOutputStream(fileEntry.file.toPath()); + OutputStreamWriter osw; + // If file encoding is specified, write using that encoding, otherwise use default platform encoding + String charsetName = fileEntry.charSetEncoding; + if(!JOrphanUtils.isBlank(charsetName)) { + osw = new OutputStreamWriter(fos, charsetName); + } else { + @SuppressWarnings("DefaultCharset") + final OutputStreamWriter withPlatformEncoding = new OutputStreamWriter(fos); + osw = withPlatformEncoding; + } + return new BufferedWriter(osw); + } + + public synchronized void closeFiles() throws IOException { + for (Map.Entry me : files.entrySet()) { + closeFile(me.getKey(),me.getValue() ); + } + files.clear(); + } + + /** + * @param name the name or alias of the file to be closed + * @throws IOException when closing of the aliased file fails + */ + public synchronized void closeFile(String name) throws IOException { + FileEntry fileEntry = files.get(name); + closeFile(name, fileEntry); + } + + private void closeFile(String name, FileEntry fileEntry) throws IOException { + if (fileEntry != null && fileEntry.inputOutputObject != null) { + log.info("Close: {}", name); + fileEntry.inputOutputObject.close(); + fileEntry.inputOutputObject = null; + } + } + + boolean filesOpen() { // package access for test code only + return files.values().stream() + .anyMatch(fileEntry -> fileEntry.inputOutputObject != null); + } + + /** + * Method will get a random file in a base directory + *

+ * TODO hey, not sure this method belongs here. + * FileServer is for thread safe File access relative to current test's base directory. + * + * @param basedir name of the directory in which the files can be found + * @param extensions array of allowed extensions, if null is given, + * any file be allowed + * @return a random File from the basedir that matches one of + * the extensions + */ + public File getRandomFile(String basedir, String[] extensions) { + File input = null; + if (basedir != null) { + File src = new File(basedir); + File[] lfiles = src.listFiles(new JMeterFileFilter(extensions)); + if (lfiles != null) { + // lfiles cannot be null as it has been checked before + int count = lfiles.length; + input = lfiles[ThreadLocalRandom.current().nextInt(count)]; + } + } + return input; + } + + /** + * Get {@link File} instance for provided file path, + * resolve file location relative to base dir or script dir when needed + * + * @param path original path to file, maybe relative + * @return {@link File} instance + */ + public File getResolvedFile(String path) { + reserveFile(path); + return files.get(path).file; + } + + private static class FileEntry{ + private String headerLine; + private Throwable exception; + private final File file; + private Closeable inputOutputObject; + private final String charSetEncoding; + + FileEntry(File f, Closeable o, String e) { + file = f; + inputOutputObject = o; + charSetEncoding = e; + } + } + + /** + * Resolve a file name that may be relative to the base directory. If the + * name begins with the value of the JMeter property + * "jmeter.save.saveservice.base_prefix" - default "~/" - then the name is + * assumed to be relative to the basename. + * + * @param relativeName + * filename that should be checked for + * jmeter.save.saveservice.base_prefix + * @return the updated filename + */ + public static String resolveBaseRelativeName(String relativeName) { + if (relativeName.startsWith(BASE_PREFIX)){ + String newName = relativeName.substring(BASE_PREFIX.length()); + return new File(getFileServer().getBaseDir(),newName).getAbsolutePath(); + } + return relativeName; + } + + /** + * @return JMX Script name + * @since 2.6 + */ + public String getScriptName() { + return scriptName; + } + + /** + * @param scriptName Script name + * @since 2.6 + */ + public void setScriptName(String scriptName) { + this.scriptName = scriptName; + } +} diff --git a/frontend/src/business/components/xpack b/frontend/src/business/components/xpack index e50f046382..e7709b9a34 160000 --- a/frontend/src/business/components/xpack +++ b/frontend/src/business/components/xpack @@ -1 +1 @@ -Subproject commit e50f0463826ac4d7837ea3a237333827774a1b19 +Subproject commit e7709b9a340394e78610b91105b2cec0f1b8289d