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; using Canon.Core.SyntaxNodes;
namespace Canon.Core.SemanticParser; namespace Canon.Core.SemanticParser;
public class CCodeGenerateVisitor : TypeCheckVisitor public class CCodeGenerateVisitor(ICompilerLogger? logger = null) : TypeCheckVisitor(logger)
{ {
public CCodeBuilder Builder { get; } = new(); public CCodeBuilder Builder { get; } = new();

View File

@ -7,7 +7,7 @@ using Expression = Canon.Core.SyntaxNodes.Expression;
namespace Canon.Core.SemanticParser; 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(); public SymbolTable SymbolTable { get; private set; } = new();

View File

@ -20,6 +20,9 @@ public class CompileResponse
[Required] [Required]
public string CompileTime { get; set; } public string CompileTime { get; set; }
[Required]
public string CompileInformation { get; set; }
public CompileResponse() public CompileResponse()
{ {
Id = string.Empty; Id = string.Empty;
@ -27,6 +30,7 @@ public class CompileResponse
CompiledCode = string.Empty; CompiledCode = string.Empty;
ImageAddress = string.Empty; ImageAddress = string.Empty;
CompileTime = string.Empty; CompileTime = string.Empty;
CompileInformation = string.Empty;
} }
public CompileResponse(CompileResult result) public CompileResponse(CompileResult result)
@ -36,5 +40,6 @@ public class CompileResponse
CompiledCode = result.CompiledCode; CompiledCode = result.CompiledCode;
ImageAddress = $"/api/file/{result.SytaxTreeImageFilename}"; ImageAddress = $"/api/file/{result.SytaxTreeImageFilename}";
CompileTime = result.CompileTime.AddHours(8).ToString("yyyy-MM-dd HH:mm:ss"); 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 CompiledCode { get; set; } = string.Empty;
public string CompileInformation { get; set; } = string.Empty;
public DateTime CompileTime { get; set; } 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.LexicalParser;
using Canon.Core.SemanticParser; using Canon.Core.SemanticParser;
using Canon.Server.Extensions; using Canon.Server.Extensions;
using Canon.Server.Models;
using Canon.Server.Services; using Canon.Server.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -21,11 +22,13 @@ builder.Services.AddDbContext<CompileDbContext>(options =>
options.UseMongoDB(connectionString, "Canon"); options.UseMongoDB(connectionString, "Canon");
}); });
builder.Services.AddGridFs(connectionString, "Canon"); builder.Services.AddGridFs(connectionString, "Canon");
builder.Services.AddSingleton<ICompilerLogger, CompilerLogger>();
builder.Services.AddTransient<ILexer, Lexer>(); builder.Services.AddTransient<ILexer, Lexer>();
builder.Services.AddSingleton<IGrammarParser>( builder.Services.AddSingleton<IGrammarParser>(
_ => GeneratedGrammarParser.Instance); _ => GeneratedGrammarParser.Instance);
builder.Services.AddSingleton<SyntaxTreePresentationService>(); builder.Services.AddSingleton<SyntaxTreePresentationService>();
builder.Services.AddSingleton<SyntaxTreeTraveller>(); builder.Services.AddSingleton<SyntaxTreeTraveller>();
builder.Services.AddTransient<CCodeGenerateVisitor>();
builder.Services.AddTransient<CompilerService>(); builder.Services.AddTransient<CompilerService>();
builder.Services.AddHostedService<DatabaseSetupService>(); builder.Services.AddHostedService<DatabaseSetupService>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,16 @@
using Microsoft.Extensions.Logging; using Canon.Core.Abstractions;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace Canon.Tests.Utils; 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, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter) 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 string Build() => string.Empty;
public void Dispose()
{
}
public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
return this;
}
} }