Extract ScratchCompilingSession to allow calling executor multiple times
This commit is contained in:
+13
-153
@@ -16,186 +16,42 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.idea.scratch.compile
|
package org.jetbrains.kotlin.idea.scratch.compile
|
||||||
|
|
||||||
import com.intellij.execution.configurations.GeneralCommandLine
|
|
||||||
import com.intellij.execution.process.CapturingProcessHandler
|
|
||||||
import com.intellij.execution.process.ProcessOutput
|
import com.intellij.execution.process.ProcessOutput
|
||||||
import com.intellij.openapi.module.Module
|
|
||||||
import com.intellij.openapi.progress.ProgressIndicator
|
|
||||||
import com.intellij.openapi.progress.Task
|
|
||||||
import com.intellij.openapi.project.DumbService
|
|
||||||
import com.intellij.openapi.util.Computable
|
|
||||||
import com.intellij.openapi.util.io.FileUtil
|
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
|
|
||||||
import org.jetbrains.kotlin.codegen.CompilationErrorHandler
|
|
||||||
import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
|
|
||||||
import org.jetbrains.kotlin.codegen.filterClassFiles
|
|
||||||
import org.jetbrains.kotlin.codegen.state.GenerationState
|
|
||||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
|
||||||
import org.jetbrains.kotlin.diagnostics.Severity
|
import org.jetbrains.kotlin.diagnostics.Severity
|
||||||
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
|
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
|
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
|
||||||
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager
|
|
||||||
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils
|
|
||||||
import org.jetbrains.kotlin.idea.refactoring.getLineNumber
|
import org.jetbrains.kotlin.idea.refactoring.getLineNumber
|
||||||
import org.jetbrains.kotlin.idea.scratch.*
|
import org.jetbrains.kotlin.idea.scratch.*
|
||||||
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutput
|
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutput
|
||||||
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputType
|
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputType
|
||||||
import org.jetbrains.kotlin.idea.util.JavaParametersBuilder
|
|
||||||
import org.jetbrains.kotlin.idea.util.application.runReadAction
|
import org.jetbrains.kotlin.idea.util.application.runReadAction
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.KtFile
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
|
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
|
||||||
import org.jetbrains.kotlin.resolve.AnalyzingUtils
|
import org.jetbrains.kotlin.resolve.AnalyzingUtils
|
||||||
import java.io.File
|
|
||||||
import kotlin.script.experimental.api.valueOrNull
|
|
||||||
|
|
||||||
class KtCompilingExecutor(file: ScratchFile) : ScratchExecutor(file) {
|
class KtCompilingExecutor(file: ScratchFile) : ScratchExecutor(file) {
|
||||||
companion object {
|
private var session: KtScratchExecutionSession? = null
|
||||||
private val TIMEOUT_MS = 30000
|
|
||||||
}
|
|
||||||
|
|
||||||
private var backgroundProcessIndicator: ProgressIndicator? = null
|
|
||||||
|
|
||||||
private val expressions = file.getExpressions()
|
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
handler.onStart(file)
|
handler.onStart(file)
|
||||||
|
|
||||||
val psiFile = file.getPsiFile() as? KtFile ?: return errorOccurs("Couldn't find KtFile for current editor", isFatal = true)
|
session = KtScratchExecutionSession(file, this)
|
||||||
|
session?.execute {
|
||||||
if (!checkForErrors(psiFile)) return
|
handler.onFinish(file)
|
||||||
|
session = null
|
||||||
val result = runReadAction { KtScratchSourceFileProcessor().process(expressions) }
|
|
||||||
when (result) {
|
|
||||||
is KtScratchSourceFileProcessor.Result.Error -> return errorOccurs(result.message, isFatal = true)
|
|
||||||
is KtScratchSourceFileProcessor.Result.OK -> {
|
|
||||||
LOG.printDebugMessage("After processing by KtScratchSourceFileProcessor:\n ${result.code}")
|
|
||||||
|
|
||||||
object : Task.Backgroundable(psiFile.project, "Running Kotlin Scratch...", true) {
|
|
||||||
override fun run(indicator: ProgressIndicator) {
|
|
||||||
backgroundProcessIndicator = indicator
|
|
||||||
|
|
||||||
val modifiedScratchSourceFile = runReadAction {
|
|
||||||
KtPsiFactory(psiFile.project).createFileWithLightClassSupport("tmp.kt", result.code, psiFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val tempDir = DumbService.getInstance(project).runReadActionInSmartMode(
|
|
||||||
Computable {
|
|
||||||
compileFileToTempDir(modifiedScratchSourceFile)
|
|
||||||
}
|
|
||||||
) ?: return
|
|
||||||
|
|
||||||
try {
|
|
||||||
val commandLine = createCommandLine(psiFile, file.getModule(), result.mainClassName, tempDir.path)
|
|
||||||
|
|
||||||
LOG.printDebugMessage(commandLine.commandLineString)
|
|
||||||
|
|
||||||
val processHandler = CapturingProcessHandler(commandLine)
|
|
||||||
val executionResult = processHandler.runProcessWithProgressIndicator(indicator, TIMEOUT_MS)
|
|
||||||
when {
|
|
||||||
executionResult.isTimeout -> {
|
|
||||||
errorOccurs("Couldn't get scratch execution result - stopped by timeout ($TIMEOUT_MS ms)")
|
|
||||||
}
|
|
||||||
executionResult.isCancelled -> {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
ProcessOutputParser().parse(executionResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
tempDir.delete()
|
|
||||||
handler.onFinish(file)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
LOG.printDebugMessage(result.code)
|
|
||||||
errorOccurs(e.message ?: "Couldn't compile ${psiFile.name}", e, isFatal = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.queue()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
try {
|
try {
|
||||||
backgroundProcessIndicator?.cancel()
|
session?.stop()
|
||||||
} finally {
|
} finally {
|
||||||
handler.onFinish(file)
|
handler.onFinish(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compileFileToTempDir(psiFile: KtFile): File? {
|
fun checkForErrors(psiFile: KtFile, expressions: List<ScratchExpression>): Boolean {
|
||||||
if (!checkForErrors(psiFile)) return null
|
|
||||||
|
|
||||||
val resolutionFacade = psiFile.getResolutionFacade()
|
|
||||||
val (bindingContext, files) = DebuggerUtils.analyzeInlinedFunctions(resolutionFacade, psiFile, false)
|
|
||||||
|
|
||||||
LOG.printDebugMessage("Analyzed files: \n${files.joinToString("\n") { it.virtualFilePath }}")
|
|
||||||
|
|
||||||
val generateClassFilter = object : GenerationState.GenerateClassFilter() {
|
|
||||||
override fun shouldGeneratePackagePart(ktFile: KtFile) = ktFile == psiFile
|
|
||||||
override fun shouldAnnotateClass(processingClassOrObject: KtClassOrObject) = true
|
|
||||||
override fun shouldGenerateClass(processingClassOrObject: KtClassOrObject) = processingClassOrObject.containingKtFile == psiFile
|
|
||||||
override fun shouldGenerateScript(script: KtScript) = false
|
|
||||||
override fun shouldGenerateCodeFragment(script: KtCodeFragment) = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = GenerationState.Builder(
|
|
||||||
file.project,
|
|
||||||
ClassBuilderFactories.BINARIES,
|
|
||||||
resolutionFacade.moduleDescriptor,
|
|
||||||
bindingContext,
|
|
||||||
files,
|
|
||||||
CompilerConfiguration.EMPTY
|
|
||||||
).generateDeclaredClassFilter(generateClassFilter).build()
|
|
||||||
|
|
||||||
KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION)
|
|
||||||
|
|
||||||
return writeClassFilesToTempDir(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun writeClassFilesToTempDir(state: GenerationState): File {
|
|
||||||
val classFiles = state.factory.asList().filterClassFiles()
|
|
||||||
|
|
||||||
val dir = FileUtil.createTempDirectory("compile", "scratch")
|
|
||||||
|
|
||||||
LOG.printDebugMessage("Temp output dir: ${dir.path}")
|
|
||||||
|
|
||||||
for (classFile in classFiles) {
|
|
||||||
val tmpOutFile = File(dir, classFile.relativePath)
|
|
||||||
tmpOutFile.parentFile.mkdirs()
|
|
||||||
tmpOutFile.createNewFile()
|
|
||||||
tmpOutFile.writeBytes(classFile.asByteArray())
|
|
||||||
|
|
||||||
LOG.printDebugMessage("Generated class file: ${classFile.relativePath}")
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCommandLine(originalFile: KtFile, module: Module?, mainClassName: String, tempOutDir: String): GeneralCommandLine {
|
|
||||||
val javaParameters = JavaParametersBuilder(originalFile.project)
|
|
||||||
.withSdkFrom(module, true)
|
|
||||||
.withMainClassName(mainClassName)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
javaParameters.classPath.add(tempOutDir)
|
|
||||||
|
|
||||||
if (module != null) {
|
|
||||||
javaParameters.classPath.addAll(JavaParametersBuilder.getModuleDependencies(module))
|
|
||||||
}
|
|
||||||
|
|
||||||
ScriptDependenciesManager.getInstance(originalFile.project)
|
|
||||||
.getRefinedCompilationConfiguration(originalFile.virtualFile)?.valueOrNull()?.let {
|
|
||||||
javaParameters.classPath.addAll(it.dependenciesClassPath.map { f -> f.absolutePath })
|
|
||||||
}
|
|
||||||
|
|
||||||
return javaParameters.toCommandLine()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkForErrors(psiFile: KtFile): Boolean {
|
|
||||||
return runReadAction {
|
return runReadAction {
|
||||||
try {
|
try {
|
||||||
AnalyzingUtils.checkForSyntacticErrors(psiFile)
|
AnalyzingUtils.checkForSyntacticErrors(psiFile)
|
||||||
@@ -241,6 +97,10 @@ class KtCompilingExecutor(file: ScratchFile) : ScratchExecutor(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parseOutput(processOutput: ProcessOutput, expressions: List<ScratchExpression>) {
|
||||||
|
ProcessOutputParser(expressions).parse(processOutput)
|
||||||
|
}
|
||||||
|
|
||||||
private fun List<ScratchExpression>.findExpression(psiElement: PsiElement): ScratchExpression? {
|
private fun List<ScratchExpression>.findExpression(psiElement: PsiElement): ScratchExpression? {
|
||||||
val elementLine = psiElement.getLineNumber()
|
val elementLine = psiElement.getLineNumber()
|
||||||
return runReadAction { firstOrNull { elementLine in it.lineStart..it.lineEnd } }
|
return runReadAction { firstOrNull { elementLine in it.lineStart..it.lineEnd } }
|
||||||
@@ -250,7 +110,7 @@ class KtCompilingExecutor(file: ScratchFile) : ScratchExecutor(file) {
|
|||||||
return runReadAction { firstOrNull { it.lineStart == lineStart && it.lineEnd == lineEnd } }
|
return runReadAction { firstOrNull { it.lineStart == lineStart && it.lineEnd == lineEnd } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ProcessOutputParser {
|
private inner class ProcessOutputParser(private val expressions: List<ScratchExpression>) {
|
||||||
fun parse(processOutput: ProcessOutput) {
|
fun parse(processOutput: ProcessOutput) {
|
||||||
val out = processOutput.stdout
|
val out = processOutput.stdout
|
||||||
val err = processOutput.stderr
|
val err = processOutput.stderr
|
||||||
|
|||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.scratch.compile
|
||||||
|
|
||||||
|
import com.intellij.execution.configurations.GeneralCommandLine
|
||||||
|
import com.intellij.execution.process.CapturingProcessHandler
|
||||||
|
import com.intellij.openapi.module.Module
|
||||||
|
import com.intellij.openapi.progress.ProgressIndicator
|
||||||
|
import com.intellij.openapi.progress.Task
|
||||||
|
import com.intellij.openapi.project.DumbService
|
||||||
|
import com.intellij.openapi.util.Computable
|
||||||
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
|
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
|
||||||
|
import org.jetbrains.kotlin.codegen.CompilationErrorHandler
|
||||||
|
import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
|
||||||
|
import org.jetbrains.kotlin.codegen.filterClassFiles
|
||||||
|
import org.jetbrains.kotlin.codegen.state.GenerationState
|
||||||
|
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||||
|
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
||||||
|
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils
|
||||||
|
import org.jetbrains.kotlin.idea.scratch.LOG
|
||||||
|
import org.jetbrains.kotlin.idea.scratch.ScratchExpression
|
||||||
|
import org.jetbrains.kotlin.idea.scratch.ScratchFile
|
||||||
|
import org.jetbrains.kotlin.idea.scratch.printDebugMessage
|
||||||
|
import org.jetbrains.kotlin.idea.util.JavaParametersBuilder
|
||||||
|
import org.jetbrains.kotlin.idea.util.application.runReadAction
|
||||||
|
import org.jetbrains.kotlin.psi.*
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.script.experimental.api.valueOrNull
|
||||||
|
|
||||||
|
class KtScratchExecutionSession(
|
||||||
|
private val file: ScratchFile,
|
||||||
|
private val executor: KtCompilingExecutor
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val TIMEOUT_MS = 30000
|
||||||
|
}
|
||||||
|
|
||||||
|
private var backgroundProcessIndicator: ProgressIndicator? = null
|
||||||
|
|
||||||
|
fun execute(callback: () -> Unit) {
|
||||||
|
val psiFile = file.getPsiFile() as? KtFile ?: return executor.errorOccurs("Couldn't find KtFile for current editor", isFatal = true)
|
||||||
|
|
||||||
|
val expressions = file.getExpressions()
|
||||||
|
if (!executor.checkForErrors(psiFile, expressions)) return
|
||||||
|
|
||||||
|
val result = runReadAction { KtScratchSourceFileProcessor().process(expressions) }
|
||||||
|
when (result) {
|
||||||
|
is KtScratchSourceFileProcessor.Result.Error -> return executor.errorOccurs(result.message, isFatal = true)
|
||||||
|
is KtScratchSourceFileProcessor.Result.OK -> {
|
||||||
|
LOG.printDebugMessage("After processing by KtScratchSourceFileProcessor:\n ${result.code}")
|
||||||
|
|
||||||
|
object : Task.Backgroundable(psiFile.project, "Running Kotlin Scratch...", true) {
|
||||||
|
override fun run(indicator: ProgressIndicator) {
|
||||||
|
backgroundProcessIndicator = indicator
|
||||||
|
|
||||||
|
val modifiedScratchSourceFile = runReadAction {
|
||||||
|
KtPsiFactory(psiFile.project).createFileWithLightClassSupport("tmp.kt", result.code, psiFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val tempDir = DumbService.getInstance(project).runReadActionInSmartMode(
|
||||||
|
Computable {
|
||||||
|
compileFileToTempDir(modifiedScratchSourceFile, expressions)
|
||||||
|
}
|
||||||
|
) ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val commandLine = createCommandLine(psiFile, file.getModule(), result.mainClassName, tempDir.path)
|
||||||
|
|
||||||
|
LOG.printDebugMessage(commandLine.commandLineString)
|
||||||
|
|
||||||
|
val processHandler = CapturingProcessHandler(commandLine)
|
||||||
|
val executionResult = processHandler.runProcessWithProgressIndicator(indicator, TIMEOUT_MS)
|
||||||
|
when {
|
||||||
|
executionResult.isTimeout -> {
|
||||||
|
executor.errorOccurs("Couldn't get scratch execution result - stopped by timeout ($TIMEOUT_MS ms)")
|
||||||
|
}
|
||||||
|
executionResult.isCancelled -> {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
executor.parseOutput(executionResult, expressions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
tempDir.delete()
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LOG.printDebugMessage(result.code)
|
||||||
|
executor.errorOccurs(e.message ?: "Couldn't compile ${psiFile.name}", e, isFatal = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.queue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
backgroundProcessIndicator?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compileFileToTempDir(psiFile: KtFile, expressions: List<ScratchExpression>): File? {
|
||||||
|
if (!executor.checkForErrors(psiFile, expressions)) return null
|
||||||
|
|
||||||
|
val resolutionFacade = psiFile.getResolutionFacade()
|
||||||
|
val (bindingContext, files) = DebuggerUtils.analyzeInlinedFunctions(resolutionFacade, psiFile, false)
|
||||||
|
|
||||||
|
LOG.printDebugMessage("Analyzed files: \n${files.joinToString("\n") { it.virtualFilePath }}")
|
||||||
|
|
||||||
|
val generateClassFilter = object : GenerationState.GenerateClassFilter() {
|
||||||
|
override fun shouldGeneratePackagePart(ktFile: KtFile) = ktFile == psiFile
|
||||||
|
override fun shouldAnnotateClass(processingClassOrObject: KtClassOrObject) = true
|
||||||
|
override fun shouldGenerateClass(processingClassOrObject: KtClassOrObject) = processingClassOrObject.containingKtFile == psiFile
|
||||||
|
override fun shouldGenerateScript(script: KtScript) = false
|
||||||
|
override fun shouldGenerateCodeFragment(script: KtCodeFragment) = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = GenerationState.Builder(
|
||||||
|
file.project,
|
||||||
|
ClassBuilderFactories.BINARIES,
|
||||||
|
resolutionFacade.moduleDescriptor,
|
||||||
|
bindingContext,
|
||||||
|
files,
|
||||||
|
CompilerConfiguration.EMPTY
|
||||||
|
).generateDeclaredClassFilter(generateClassFilter).build()
|
||||||
|
|
||||||
|
KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION)
|
||||||
|
|
||||||
|
return writeClassFilesToTempDir(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeClassFilesToTempDir(state: GenerationState): File {
|
||||||
|
val classFiles = state.factory.asList().filterClassFiles()
|
||||||
|
|
||||||
|
val dir = FileUtil.createTempDirectory("compile", "scratch")
|
||||||
|
|
||||||
|
LOG.printDebugMessage("Temp output dir: ${dir.path}")
|
||||||
|
|
||||||
|
for (classFile in classFiles) {
|
||||||
|
val tmpOutFile = File(dir, classFile.relativePath)
|
||||||
|
tmpOutFile.parentFile.mkdirs()
|
||||||
|
tmpOutFile.createNewFile()
|
||||||
|
tmpOutFile.writeBytes(classFile.asByteArray())
|
||||||
|
|
||||||
|
LOG.printDebugMessage("Generated class file: ${classFile.relativePath}")
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCommandLine(originalFile: KtFile, module: Module?, mainClassName: String, tempOutDir: String): GeneralCommandLine {
|
||||||
|
val javaParameters = JavaParametersBuilder(originalFile.project)
|
||||||
|
.withSdkFrom(module, true)
|
||||||
|
.withMainClassName(mainClassName)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
javaParameters.classPath.add(tempOutDir)
|
||||||
|
|
||||||
|
if (module != null) {
|
||||||
|
javaParameters.classPath.addAll(JavaParametersBuilder.getModuleDependencies(module))
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptDependenciesManager.getInstance(originalFile.project)
|
||||||
|
.getRefinedCompilationConfiguration(originalFile.virtualFile)?.valueOrNull()?.let {
|
||||||
|
javaParameters.classPath.addAll(it.dependenciesClassPath.map { f -> f.absolutePath })
|
||||||
|
}
|
||||||
|
|
||||||
|
return javaParameters.toCommandLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun foo() {
|
||||||
|
listOf(1, 2, 3).joinToString {
|
||||||
|
if (true) {
|
||||||
|
"lsdkfjlsd"
|
||||||
|
} else {
|
||||||
|
"sdfsdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user