Compare commits
9 Commits
578a5c6372
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
7ceea9df71 | ||
|
b31e2049bd | ||
|
279247905c | ||
|
93450a9f49 | ||
|
41f5bc5d67 | ||
|
c5929e8cb1 | ||
|
2bea742a69 | ||
|
4309ab9aad | ||
|
1082305c64 |
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15071",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16047"
|
||||
}
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:15071"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||
}
|
@@ -7,6 +7,7 @@
|
||||
<UserSecretsId>ffc728b4-a681-4404-8156-a09f59c957d3</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<DockerComposeProjectPath>..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
|
||||
namespace BlazorCanvas.Server.Components.Data;
|
||||
|
||||
@@ -12,31 +14,45 @@ namespace BlazorCanvas.Server.Components.Data;
|
||||
|
||||
public class CanvasService {
|
||||
private Canvas2DContext? _currentCanvasContext;
|
||||
private IJSRuntime _jsRuntime;
|
||||
private IRedisService _redisService;
|
||||
private CanvasCommand _lastCommand = new();
|
||||
private bool _is_started = false;
|
||||
private List<CanvasCommand> _lsComms = new();
|
||||
private bool _is_init = false,
|
||||
_is_started = false,
|
||||
_has_ended = false;
|
||||
|
||||
public CanvasService(IJSRuntime jsRuntime, IRedisService redisService) {
|
||||
_jsRuntime = jsRuntime;
|
||||
public CanvasService(IRedisService redisService) {
|
||||
_redisService = redisService;
|
||||
}
|
||||
|
||||
~CanvasService() {
|
||||
_has_ended = true;
|
||||
Task.Delay(100);
|
||||
}
|
||||
|
||||
public string currentColor { get; set; } = "Black";
|
||||
public int pointSize { get; set; } = 1;
|
||||
public bool snap { get; set; }
|
||||
public ElementReference divCanvas { get; set; }
|
||||
public BECanvasComponent myCanvas { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Version Pub/Sub
|
||||
/// </summary>
|
||||
public async void Subscribe() {
|
||||
CancellationToken cToken = new();
|
||||
while (!cToken.IsCancellationRequested) {
|
||||
var comm = await _redisService.Subscribe(cToken);
|
||||
if (comm is not null)
|
||||
Draw(comm);
|
||||
public async void Consume() {
|
||||
while (!_has_ended) {
|
||||
if (_lsComms.Count > 0)
|
||||
_lsComms.Clear();
|
||||
_lsComms.AddRange(await _redisService.Consume());
|
||||
if (_lsComms.Count == 1)
|
||||
Draw(_lsComms[0]);
|
||||
else if (_lsComms.Count > 0)
|
||||
Draw(_lsComms);
|
||||
}
|
||||
}
|
||||
|
||||
public async void InitStreamer() {
|
||||
while (!_is_init) {
|
||||
_is_init = await _redisService.InitStreamer();
|
||||
if (!_is_init)
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +66,9 @@ public class CanvasService {
|
||||
await _currentCanvasContext.SetFillStyleAsync(command.Color);
|
||||
await _currentCanvasContext.FillRectAsync(command.X, command.Y, command.PointSize, command.PointSize);
|
||||
}
|
||||
await _currentCanvasContext.EndBatchAsync();
|
||||
try {
|
||||
await _currentCanvasContext.EndBatchAsync();
|
||||
} catch { } // HAHAHAHA ÇA MARCHE.
|
||||
}
|
||||
|
||||
public async void Draw(CanvasCommand command) {
|
||||
@@ -70,34 +88,32 @@ public class CanvasService {
|
||||
public async void HandleMouse(MouseEventArgs eventArgs) {
|
||||
double mouseX = 0, mouseY = 0;
|
||||
|
||||
if (!_is_started) {
|
||||
Subscribe();
|
||||
_is_started = true;
|
||||
if (!_is_started) {
|
||||
if (!_is_init)
|
||||
InitStreamer();
|
||||
if (_is_init) {
|
||||
Consume();
|
||||
_is_started = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventArgs.Buttons == 0 || eventArgs.Buttons > 2)
|
||||
return; // Rien faire si aucun bouton est appuyé ou si les deux boutons/ d'autres boutons sont appuyés.
|
||||
|
||||
if (divCanvas.Id?.Length > 0) {
|
||||
string data = await _jsRuntime.InvokeAsync<string>("getDivCanvasOffsets",
|
||||
new object[] { divCanvas });
|
||||
string color = "White";
|
||||
CanvasCommand command = new();
|
||||
JObject? offsets = (JObject?)JsonConvert.DeserializeObject(data);
|
||||
|
||||
if (offsets is not null && offsets.HasValues) { // Translation entre le canvas et la souris.
|
||||
mouseX = eventArgs.PageX - offsets.Value<double>("offsetLeft");
|
||||
mouseY = eventArgs.PageY - offsets.Value<double>("offsetTop");
|
||||
}
|
||||
// Magnétisme boboche si activé, centrage si pas activé.
|
||||
mouseX = eventArgs.OffsetX;
|
||||
mouseX -= (!snap? pointSize / 2: mouseX % pointSize);
|
||||
mouseY = eventArgs.OffsetY;
|
||||
mouseY -= (!snap? pointSize / 2: mouseY % pointSize);
|
||||
|
||||
if (eventArgs.Buttons == 1) // Couleur si bouton gauche, blanc si bouton droit
|
||||
color = currentColor;
|
||||
|
||||
if (snap) { // Magnétisme boboche.
|
||||
mouseX -= mouseX % pointSize;
|
||||
mouseY -= mouseY % pointSize;
|
||||
}
|
||||
|
||||
command.X = mouseX;
|
||||
command.Y = mouseY;
|
||||
command.Color = color;
|
||||
@@ -105,10 +121,8 @@ public class CanvasService {
|
||||
|
||||
if (command.Equals(_lastCommand))
|
||||
return; // Pour pas spammer des commandes si c'est pas pertinent.
|
||||
|
||||
_redisService.Publish(command);
|
||||
|
||||
//Draw(command); // Local
|
||||
_redisService.Produce(command);
|
||||
|
||||
_lastCommand = command;
|
||||
}
|
||||
|
@@ -1,6 +1,12 @@
|
||||
namespace BlazorCanvas.Server.Components.Data;
|
||||
|
||||
public interface IRedisService {
|
||||
public Task<CanvasCommand?> Subscribe(CancellationToken cToken);
|
||||
public void Publish(CanvasCommand command);
|
||||
Task<bool> InitStreamer();
|
||||
bool InitSubscriber();
|
||||
|
||||
Task<CanvasCommand?> Subscribe(CancellationToken cToken);
|
||||
void Publish(CanvasCommand command);
|
||||
|
||||
void Produce(CanvasCommand command);
|
||||
Task<IEnumerable<CanvasCommand>> Consume();
|
||||
}
|
||||
|
@@ -1,17 +1,85 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
|
||||
namespace BlazorCanvas.Server.Components.Data;
|
||||
|
||||
// https://developer.redis.com/develop/dotnet/streams/stream-basics/
|
||||
|
||||
public class RedisService: IRedisService {
|
||||
public class RedisService : IRedisService {
|
||||
private const string STREAM_NAME = "steamie",
|
||||
GROUP_NAME = "groupie",
|
||||
SUB_NAME = "servant";
|
||||
|
||||
private string _lastId = "0-0";
|
||||
|
||||
private NameValueEntry[] arNve = new NameValueEntry[1];
|
||||
|
||||
private IConnectionMultiplexer _cache;
|
||||
private IDatabase _database;
|
||||
private ChannelMessageQueue _channel;
|
||||
|
||||
public RedisService(IConnectionMultiplexer cache) {
|
||||
_cache = cache;
|
||||
_channel = _cache.GetSubscriber().Subscribe(RedisChannel.Literal("lol"));
|
||||
public RedisService(IConnectionMultiplexer cache) { _cache = cache; }
|
||||
|
||||
/// <summary>
|
||||
/// Init Pub/Sub - Redis en mode Pub/Sub ne garde pas ses messages en mémoire,
|
||||
/// donc les commandes faites avant la souscription ne sont pas copiées.
|
||||
/// </summary>
|
||||
/// <returns>Si la connexion a réussi.</returns>
|
||||
public bool InitSubscriber() {
|
||||
try {
|
||||
_channel = _cache.GetSubscriber().Subscribe(RedisChannel.Literal(SUB_NAME));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (_channel is null)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Init Streamer - Devrait être plus près de Kafka comme comportement.
|
||||
/// </summary>
|
||||
/// <returns>Si la connexion a réussi.</returns>
|
||||
public async Task<bool> InitStreamer() {
|
||||
try {
|
||||
_database = _cache.GetDatabase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (_database is null)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async void Produce(CanvasCommand command) {
|
||||
arNve[0] = new NameValueEntry("command", JsonConvert.SerializeObject(command));
|
||||
_database.StreamAddAsync(STREAM_NAME, arNve);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CanvasCommand>> Consume() {
|
||||
List<CanvasCommand> lsComm = new();
|
||||
CanvasCommand? comm;
|
||||
var result = await _database.StreamReadAsync(STREAM_NAME, _lastId, 100);
|
||||
string json = "";
|
||||
bool ok = false;
|
||||
foreach (var c in result) {
|
||||
try {
|
||||
json = c.Values.FirstOrDefault(x => x.Name == "command").Value.ToString();
|
||||
comm = JsonConvert.DeserializeObject<CanvasCommand>(json);
|
||||
} catch {
|
||||
Console.WriteLine($"OH NO {json}");
|
||||
continue; }
|
||||
if (comm is not null)
|
||||
lsComm.Add(comm);
|
||||
if (!ok)
|
||||
ok = true;
|
||||
}
|
||||
if (ok)
|
||||
_lastId = result.LastOrDefault().Id.ToString()?? "0-0";
|
||||
return lsComm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -20,10 +88,13 @@ public class RedisService: IRedisService {
|
||||
/// </summary>
|
||||
public async Task<CanvasCommand?> Subscribe(CancellationToken cToken) {
|
||||
var mess = await _channel.ReadAsync(cToken);
|
||||
var comm = JsonConvert.DeserializeObject<CanvasCommand>(mess.Message);
|
||||
CanvasCommand? comm;
|
||||
try {
|
||||
comm = JsonConvert.DeserializeObject<CanvasCommand>(mess.Message);
|
||||
} catch { return null; }
|
||||
if (comm is not null)
|
||||
return comm;
|
||||
else return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -32,7 +103,6 @@ public class RedisService: IRedisService {
|
||||
/// </summary>
|
||||
/// <param name="command">La commande à publier</param>
|
||||
public async void Publish(CanvasCommand command) {
|
||||
CanvasCommand cm = new(command);
|
||||
await _cache.GetSubscriber().PublishAsync(_channel.Channel, JsonConvert.SerializeObject(cm));
|
||||
await _cache.GetSubscriber().PublishAsync(_channel.Channel, JsonConvert.SerializeObject(command));
|
||||
}
|
||||
}
|
||||
|
125
BlazorCanvas/BlazorCanvas.Server/Docker_Deploy.yaml
Normal file
125
BlazorCanvas/BlazorCanvas.Server/Docker_Deploy.yaml
Normal file
@@ -0,0 +1,125 @@
|
||||
# First, add the API
|
||||
apiVersion: apps/v1
|
||||
# This will be the deployment setup
|
||||
kind: Deployment
|
||||
metadata:
|
||||
# Name your Deployment here
|
||||
name: blazorcanvas
|
||||
labels:
|
||||
# label your deployment
|
||||
app: BlazorCanvas-etc
|
||||
spec:
|
||||
# The number of pods/replicas to run
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
# selector to match the pod
|
||||
app: BlazorCanvas-etc
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
# label your pod
|
||||
app: BlazorCanvas-etc
|
||||
spec:
|
||||
containers:
|
||||
# Add the container name for Kubernetes
|
||||
- name: blazorcanvas
|
||||
# Add the local image name
|
||||
image: blazorcanvasserver:latest
|
||||
# never pull the image policy
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
# port for running the container, le port sur lequel l'application du conteneur roule
|
||||
- containerPort: 8080
|
||||
- containerPort: 8081
|
||||
# First, add the API
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
# This will be the deployment setup
|
||||
kind: Deployment
|
||||
metadata:
|
||||
# Name your Deployment here
|
||||
name: francis-redis
|
||||
labels:
|
||||
# label your deployment
|
||||
app: redis-app
|
||||
spec:
|
||||
# The number of pods/replicas to run
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
# selector to match the pod
|
||||
app: redis-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
# label your pod
|
||||
app: redis-app
|
||||
spec:
|
||||
containers:
|
||||
# Add the container name for Kubernetes
|
||||
# Cette image est t<>l<EFBFBD>charg<72>e en partant BlazorCanvas.AppHost et doit passer par minikube image load.
|
||||
- name: redis
|
||||
# Add the local image name
|
||||
image: redis:latest
|
||||
# never pull the image policy
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
# ports for running the container, le port sur lequel l'application du conteneur roule
|
||||
- containerPort: 6379
|
||||
env:
|
||||
- name: "REDIS_ARGS"
|
||||
value: "--appendonly yes --appendfsync everysec"
|
||||
---
|
||||
# First, add the Service API
|
||||
apiVersion: v1
|
||||
# This will be the Service setup
|
||||
kind: Service
|
||||
metadata:
|
||||
# Your service name
|
||||
name: francis-redis
|
||||
spec:
|
||||
selector:
|
||||
# selector that matches the pod
|
||||
app: redis-app
|
||||
# type of service
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
# port for exposing the service
|
||||
port: 6379
|
||||
# portfor exposing the pod
|
||||
targetPort: 6379
|
||||
# port for exposing the node
|
||||
nodePort: 31121
|
||||
---
|
||||
# First, add the Service API
|
||||
apiVersion: v1
|
||||
# This will be the Service setup
|
||||
kind: Service
|
||||
metadata:
|
||||
# Your service name
|
||||
name: blazorcanvas-srv
|
||||
spec:
|
||||
selector:
|
||||
# selector that matches the pod
|
||||
app: BlazorCanvas-etc
|
||||
# type of service
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: "http"
|
||||
# port for exposing the service
|
||||
port: 8080
|
||||
# portfor exposing the pod
|
||||
targetPort: 8080
|
||||
# port for exposing the node
|
||||
nodePort: 31122
|
||||
- protocol: TCP
|
||||
name: "https"
|
||||
# port for exposing the service
|
||||
port: 8081
|
||||
# portfor exposing the pod
|
||||
targetPort: 8081
|
||||
# port for exposing the node
|
||||
nodePort: 31123
|
@@ -5,5 +5,8 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"cache": "francis-redis"
|
||||
}
|
||||
}
|
||||
|
20
README.md
20
README.md
@@ -1,2 +1,20 @@
|
||||
# Blazor_Canvas
|
||||
[](https://classroom.github.com/a/kDHznpg1)
|
||||
# Épreuve terminale de cours
|
||||
|
||||
## CONTEXTE :
|
||||
Un riche philanthrope vous donne la chance de développer votre propre projet personnel, toute dépense payés (wow!).
|
||||
Vous avez jusqu’au 20 décembre pour assembler un MVP (Minimum Viable Product), et s’il est assez impressionné il vous engagera comme CTO (Chief Technical Officer) pour une startup qu’il fondera à ce moment-là.
|
||||
|
||||
## SPÉCIFICATIONS :
|
||||
Afin de pouvoir démontrer que votre serveur puisse être mis en grappe, la seule contrainte de l’application sera qu’au minimum 2 utilisateurs puissent interagir ensemble. Ainsi, si un des utilisateurs se connecte à 1 nœud de la grappe et qu’un autre se connecte sur le deuxième nœud, les deux devraient pouvoir utiliser les fonctionnalités comme s’il n’y avait qu’un seul serveur.
|
||||
Vous devrez choisir un sujet pour votre épreuve et venir le réserver avec moi. Il ne sera pas possible de choisir le même sujet qu’un autre étudiant, donc premier arrivé premier servi! Plus votre application est "ambitieuse", plus je serai généreux sur la correction, et à l'inverse plus l'application est simple plus je serai pénalisant si vous coupez les coins ronds.
|
||||
Côté architecture, vous êtes libres de choisir la technologie que vous désirez. Vous devrez cependant nous démontrer en personne que votre serveur puisse marcher en grappe (cluster), que si une des nœuds de votre grappe se met soudainement hors service les autres nœuds puissent prendre le relais, et que vous pouvez ajouter un autre nœud à la grappe si besoin.
|
||||
Il devra être possible d’interagir avec votre serveur à l’aide d’un interface utilisateur (UI), mais il n’est pas nécessaire que l’interface soit servie par votre cluster si vous préférez séparer votre serveur de votre interface. Par exemple : si vous décidez de développer une application mobile séparée comme interface utilisateur, vous pourrez développer votre serveur sans frontend.
|
||||
|
||||
## BARÈMES DE CORRECTION :
|
||||
1. (30%) Application fonctionnelle
|
||||
2. (30%) Votre serveur fonctionne en grappe (2 utilisateurs sur 2 nœuds différents interagissent ensemble)
|
||||
3. (20%) Si un nœud est hors-service, le reste marche correctement
|
||||
4. (20%) Il est possible d’ajouter un nœud à la grappe
|
||||
5. (-40%) Manquements à la traçabilité de votre démarche à travers les traces d'évolution du travail sur GitHub Classroom. (Je dois voir la progression évoluer à chaque cours : faites des push souvent!)
|
||||
6. (-10%) Fautes de français
|
Reference in New Issue
Block a user