feat: 在前端显示编译过程中的日志 (#67)

Reviewed-on: PostGuard/Canon#67
This commit is contained in:
jackfiled 2024-04-29 23:55:36 +08:00
parent 4d325569fa
commit 911c813996
15 changed files with 243 additions and 204 deletions

View File

@ -0,0 +1,12 @@
using Microsoft.Extensions.Logging;
namespace Canon.Core.Abstractions;
public interface ICompilerLogger : ILogger
{
IDisposable ILogger.BeginScope<TState>(TState state) => default!;
bool ILogger.IsEnabled(LogLevel logLevel) => true;
public string Build();
}

View File

@ -1,9 +1,10 @@
using Canon.Core.CodeGenerators;
using Canon.Core.Abstractions;
using Canon.Core.CodeGenerators;
using Canon.Core.SyntaxNodes;
namespace Canon.Core.SemanticParser;
public class CCodeGenerateVisitor : TypeCheckVisitor
public class CCodeGenerateVisitor(ICompilerLogger? logger = null) : TypeCheckVisitor(logger)
{
public CCodeBuilder Builder { get; } = new();

View File

@ -7,7 +7,7 @@ using Expression = Canon.Core.SyntaxNodes.Expression;
namespace Canon.Core.SemanticParser;
public class TypeCheckVisitor(ILogger<TypeCheckVisitor>? logger = null) : SyntaxNodeVisitor
public class TypeCheckVisitor(ICompilerLogger? logger = null) : SyntaxNodeVisitor
{
public SymbolTable SymbolTable { get; private set; } = new();

View File

@ -20,6 +20,9 @@ public class CompileResponse
[Required]
public string CompileTime { get; set; }
[Required]
public string CompileInformation { get; set; }
public CompileResponse()
{
Id = string.Empty;
@ -27,6 +30,7 @@ public class CompileResponse
CompiledCode = string.Empty;
ImageAddress = string.Empty;
CompileTime = string.Empty;
CompileInformation = string.Empty;
}
public CompileResponse(CompileResult result)
@ -36,5 +40,6 @@ public class CompileResponse
CompiledCode = result.CompiledCode;
ImageAddress = $"/api/file/{result.SytaxTreeImageFilename}";
CompileTime = result.CompileTime.AddHours(8).ToString("yyyy-MM-dd HH:mm:ss");
CompileInformation = result.CompileInformation;
}
}

View File

@ -17,5 +17,7 @@ public class CompileResult
public string CompiledCode { get; set; } = string.Empty;
public string CompileInformation { get; set; } = string.Empty;
public DateTime CompileTime { get; set; }
}

View File

@ -0,0 +1,30 @@
using System.Text;
using Canon.Core.Abstractions;
namespace Canon.Server.Models;
public class CompilerLogger : ICompilerLogger
{
private readonly ThreadLocal<StringBuilder> _builder = new(() => new StringBuilder());
public string Build()
{
if (_builder.Value is not null)
{
string result = _builder.Value.ToString();
_builder.Value.Clear();
return result;
}
throw new InvalidOperationException();
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (_builder.Value is not null)
{
_builder.Value.Append(logLevel).Append(": ").Append(formatter(state, exception)).Append('\n');
}
}
}

View File

@ -3,6 +3,7 @@ using Canon.Core.GrammarParser;
using Canon.Core.LexicalParser;
using Canon.Core.SemanticParser;
using Canon.Server.Extensions;
using Canon.Server.Models;
using Canon.Server.Services;
using Microsoft.EntityFrameworkCore;
@ -21,11 +22,13 @@ builder.Services.AddDbContext<CompileDbContext>(options =>
options.UseMongoDB(connectionString, "Canon");
});
builder.Services.AddGridFs(connectionString, "Canon");
builder.Services.AddSingleton<ICompilerLogger, CompilerLogger>();
builder.Services.AddTransient<ILexer, Lexer>();
builder.Services.AddSingleton<IGrammarParser>(
_ => GeneratedGrammarParser.Instance);
builder.Services.AddSingleton<SyntaxTreePresentationService>();
builder.Services.AddSingleton<SyntaxTreeTraveller>();
builder.Services.AddTransient<CCodeGenerateVisitor>();
builder.Services.AddTransient<CompilerService>();
builder.Services.AddHostedService<DatabaseSetupService>();

View File

@ -4,15 +4,10 @@ using MongoDB.EntityFrameworkCore.Extensions;
namespace Canon.Server.Services;
public class CompileDbContext : DbContext
public class CompileDbContext(DbContextOptions<CompileDbContext> options) : DbContext(options)
{
public DbSet<CompileResult> CompileResults { get; init; }
public CompileDbContext(DbContextOptions<CompileDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

View File

@ -13,6 +13,8 @@ public class CompilerService(
ILexer lexer,
IGrammarParser grammarParser,
SyntaxTreeTraveller traveller,
CCodeGenerateVisitor visitor,
ICompilerLogger compilerLogger,
CompileDbContext dbContext,
GridFsService gridFsService,
SyntaxTreePresentationService syntaxTreePresentationService,
@ -39,7 +41,6 @@ public class CompilerService(
await using Stream imageStream = syntaxTreePresentationService.Present(root);
string filename = await gridFsService.UploadStream(imageStream, "image/png");
CCodeGenerateVisitor visitor = new();
traveller.Travel(root, visitor);
CompileResult result = new()
@ -48,7 +49,8 @@ public class CompilerService(
CompileId = Guid.NewGuid().ToString(),
CompiledCode = visitor.Builder.Build(),
SytaxTreeImageFilename = filename,
CompileTime = DateTime.Now
CompileTime = DateTime.Now,
CompileInformation = compilerLogger.Build()
};
await dbContext.CompileResults.AddAsync(result);

View File

@ -1,9 +1,5 @@
import * as openapi from '../openapi';
export interface OutputIntf {
compiledCode: string,
id: string,
imageAddress: string,
sourceCode: string,
compileTime: string
data : openapi.components["schemas"]["CompileResponse"]
}

View File

@ -7,8 +7,6 @@ import * as openapi from '../openapi';
import {enqueueSnackbar} from "notistack";
import {useNavigate} from "react-router-dom";
import {HistoryPage} from "./HistoryPage.tsx";
import {OutputIntf} from "../Interfaces/OutputIntf.ts";
const client = createClient<openapi.paths>();
@ -16,12 +14,13 @@ const client = createClient<openapi.paths>();
export function Index() {
const [inputValue, setInputValue] = useState('');
const [outputValue, setOutputValue] = useState<OutputIntf>({
const [outputValue, setOutputValue] = useState<openapi.components["schemas"]["CompileResponse"]>({
compiledCode: "",
sourceCode: "",
id: "",
imageAddress: "",
compileTime: ""
compileTime: "",
compileInformation: ""
});
const [historyPageState,setHistoryPageState] = useState(false);
const navigate = useNavigate(); // 跳转hook
@ -36,7 +35,8 @@ export function Index() {
sourceCode: "",
id: "",
imageAddress: "pic/uncompiled.png",
compileTime: ""
compileTime: "",
compileInformation: ""
})
return;
}
@ -52,13 +52,7 @@ export function Index() {
})
if (data !== undefined) {
setInputValue(data.sourceCode);
setOutputValue({
compiledCode: data.compiledCode,
sourceCode: data.sourceCode,
id: data.id,
imageAddress: data.imageAddress,
compileTime: data.compileTime
})
setOutputValue(data)
}
}
getCompileInstance();
@ -79,13 +73,7 @@ export function Index() {
})
if (data !== undefined) {
setOutputValue({
compiledCode: data.compiledCode,
sourceCode: data.sourceCode,
id: data.id,
imageAddress: data.imageAddress,
compileTime: data.compileTime
})
setOutputValue(data);
enqueueSnackbar("编译成功", {variant: "success", anchorOrigin: {vertical: 'bottom', horizontal: 'right'}});
navigate(`/${data.id}`, {})

View File

@ -1,13 +1,14 @@
import {CSSProperties, useState} from "react";
import {Box, ToggleButton, ToggleButtonGroup} from "@mui/material";
import {PhotoProvider, PhotoView} from "react-photo-view";
import { CSSProperties, useState } from "react";
import { Box, ToggleButton, ToggleButtonGroup } from "@mui/material";
import { PhotoProvider, PhotoView } from "react-photo-view";
import MonacoEditor from "react-monaco-editor";
import { OutputIntf } from "../Interfaces/OutputIntf";
// @ts-expect-error ...
export function OutputField({data}) {
export function OutputField(props: OutputIntf) {
const [state, setState] = useState('tree')
const {imageAddress, compiledCode} = data;
const { imageAddress, compiledCode, compileInformation } = props.data;
return <>
<div className={"output-field"} style={outputFieldClassCss}>
<ToggleButtonGroup
@ -17,7 +18,7 @@ export function OutputField({data}) {
position: "relative",
top: "0",
left: "50%",
height : "10%",
height: "10%",
paddingBottom: "5%",
transform: "translateX(-50%)"
}}
@ -28,49 +29,60 @@ export function OutputField({data}) {
aria-label="Platform"
>
<ToggleButton value="code"
aria-label="code"
size={"small"}>
aria-label="code"
size={"small"}>
Code
</ToggleButton>
<ToggleButton value="tree"
aria-label="tree"
size={"small"}>
aria-label="tree"
size={"small"}>
Tree
</ToggleButton>
<ToggleButton value="log" aria-label="log" size={"small"}>
Log
</ToggleButton>
</ToggleButtonGroup>
<Box sx = {{
<Box sx={{
height: "90%",
}}>
{
state === 'tree' ?
{
state === 'tree' &&
<PhotoProvider>
<PhotoView key={1} src={imageAddress}>
{imageAddress == "pic/uncompiled.png" ?
<img src={imageAddress}
style={{
width: "100%",
height: "auto"
}}
alt=""/> :
style={{
width: "100%",
height: "auto"
}}
alt="" /> :
<img src={imageAddress}
style={{
objectFit: 'cover',
width: "100%",
height: "100%"
}}
alt=""/>
style={{
objectFit: 'cover',
width: "100%",
height: "100%"
}}
alt="" />
}
</PhotoView>
</PhotoProvider>
: <MonacoEditor
language="javascript"
theme="twilight"
value={compiledCode === "" ? "也就是说,还没编译啊还没编译" : compiledCode}
options={{readOnly:true}}
/>
}
}
{
state == "code" && <MonacoEditor
language="javascript"
theme="twilight"
value={compiledCode === "" ? "也就是说,还没编译啊还没编译" : compiledCode}
options={{ readOnly: true }}
/>
}
{
state == "log" && <MonacoEditor
theme={"twilight"}
value={compileInformation}
options={{readOnly: true}}
/>
}
</Box>
</div>
</>

View File

@ -5,139 +5,140 @@
export interface paths {
"/api/Compiler": {
get: {
parameters: {
query?: {
start?: number;
end?: number;
};
};
responses: {
/** @description Success */
200: {
content: {
"text/plain": components["schemas"]["CompileResponse"][];
"application/json": components["schemas"]["CompileResponse"][];
"text/json": components["schemas"]["CompileResponse"][];
};
};
};
"/api/Compiler": {
get: {
parameters: {
query?: {
start?: number;
end?: number;
};
post: {
requestBody?: {
content: {
"application/json": components["schemas"]["SourceCode"];
"text/json": components["schemas"]["SourceCode"];
"application/*+json": components["schemas"]["SourceCode"];
};
};
responses: {
/** @description Success */
200: {
content: {
"text/plain": components["schemas"]["CompileResponse"];
"application/json": components["schemas"]["CompileResponse"];
"text/json": components["schemas"]["CompileResponse"];
};
};
};
};
delete: {
responses: {
/** @description No Content */
204: {
content: never;
};
};
};
responses: {
/** @description Success */
200: {
content: {
"text/plain": components["schemas"]["CompileResponse"][];
"application/json": components["schemas"]["CompileResponse"][];
"text/json": components["schemas"]["CompileResponse"][];
};
};
};
};
"/api/Compiler/{compileId}": {
get: {
parameters: {
path: {
compileId: string;
};
};
responses: {
/** @description Success */
200: {
content: {
"text/plain": components["schemas"]["CompileResponse"];
"application/json": components["schemas"]["CompileResponse"];
"text/json": components["schemas"]["CompileResponse"];
};
};
};
post: {
requestBody?: {
content: {
"application/json": components["schemas"]["SourceCode"];
"text/json": components["schemas"]["SourceCode"];
"application/*+json": components["schemas"]["SourceCode"];
};
delete: {
parameters: {
path: {
compileId: string;
};
};
responses: {
/** @description No Content */
204: {
content: never;
};
/** @description Not Found */
404: {
content: {
"text/plain": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ProblemDetails"];
"text/json": components["schemas"]["ProblemDetails"];
};
};
};
};
responses: {
/** @description Success */
200: {
content: {
"text/plain": components["schemas"]["CompileResponse"];
"application/json": components["schemas"]["CompileResponse"];
"text/json": components["schemas"]["CompileResponse"];
};
};
};
};
"/api/File/{filename}": {
get: {
parameters: {
path: {
filename: string;
};
};
responses: {
/** @description Success */
200: {
content: never;
};
};
delete: {
responses: {
/** @description No Content */
204: {
content: never;
};
};
};
};
"/api/Compiler/{compileId}": {
get: {
parameters: {
path: {
compileId: string;
};
};
responses: {
/** @description Success */
200: {
content: {
"text/plain": components["schemas"]["CompileResponse"];
"application/json": components["schemas"]["CompileResponse"];
"text/json": components["schemas"]["CompileResponse"];
};
};
};
};
delete: {
parameters: {
path: {
compileId: string;
};
};
responses: {
/** @description No Content */
204: {
content: never;
};
/** @description Not Found */
404: {
content: {
"text/plain": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ProblemDetails"];
"text/json": components["schemas"]["ProblemDetails"];
};
};
};
};
};
"/api/File/{filename}": {
get: {
parameters: {
path: {
filename: string;
};
};
responses: {
/** @description Success */
200: {
content: never;
};
};
};
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
CompileResponse: {
id: string;
sourceCode: string;
compiledCode: string;
imageAddress: string;
compileTime: string;
};
ProblemDetails: {
type?: string | null;
title?: string | null;
/** Format: int32 */
status?: number | null;
detail?: string | null;
instance?: string | null;
[key: string]: unknown;
};
SourceCode: {
code: string;
};
schemas: {
CompileResponse: {
id: string;
sourceCode: string;
compiledCode: string;
imageAddress: string;
compileTime: string;
compileInformation: string;
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
ProblemDetails: {
type?: string | null;
title?: string | null;
/** Format: int32 */
status?: number | null;
detail?: string | null;
instance?: string | null;
[key: string]: unknown;
};
SourceCode: {
code: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;

View File

@ -7,7 +7,7 @@ namespace Canon.Tests.SemanticTests;
public class TypeCheckVisitorTests(ITestOutputHelper testOutputHelper)
{
private readonly TestLogger<TypeCheckVisitor> _logger = new(testOutputHelper);
private readonly TestLogger _logger = new(testOutputHelper);
[Fact]
public void ConstTypeTest()

View File

@ -1,24 +1,16 @@
using Microsoft.Extensions.Logging;
using Canon.Core.Abstractions;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Canon.Tests.Utils;
public class TestLogger<T>(ITestOutputHelper testOutputHelper) : ILogger<T>, IDisposable
public class TestLogger(ITestOutputHelper testOutputHelper) : ICompilerLogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
testOutputHelper.WriteLine("{0}: {1}", logLevel, formatter(state, exception));
testOutputHelper.WriteLine($"{logLevel}: {formatter(state, exception)}");
}
public bool IsEnabled(LogLevel logLevel) => false;
public void Dispose()
{
}
public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
return this;
}
public string Build() => string.Empty;
}