1
0

Compare commits

...

9 Commits

Author SHA1 Message Date
MarcEricMartel
7ceea9df71 Erratas. 2023-11-19 12:15:42 -05:00
MarcEricMartel
b31e2049bd Hallelujah! 2023-11-19 12:06:26 -05:00
MarcEricMartel
279247905c Ready, Francis? 2023-11-19 11:48:00 -05:00
MarcEricMartel
93450a9f49 OH YEAH. 2023-11-19 10:54:37 -05:00
MarcEricMartel
41f5bc5d67 It's time to let your babies grow up to be cowboys. 🎶 2023-11-18 15:03:51 -05:00
MarcEricMartel
c5929e8cb1 Erratum 2023-11-18 12:46:31 -05:00
MarcEricMartel
2bea742a69 Ajouts du mode stream + ajouts QoL. 2023-11-18 12:19:37 -05:00
MarcEricMartel
4309ab9aad woohoo 2023-11-17 20:40:46 -05:00
MarcEricMartel
1082305c64 Update README.md 2023-11-16 11:45:29 -05:00
8 changed files with 287 additions and 50 deletions

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:15071",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16047" "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16047"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:15071"
} }
} },
} "$schema": "http://json.schemastore.org/launchsettings.json"
} }

View File

@@ -7,6 +7,7 @@
<UserSecretsId>ffc728b4-a681-4404-8156-a09f59c957d3</UserSecretsId> <UserSecretsId>ffc728b4-a681-4404-8156-a09f59c957d3</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext> <DockerfileContext>..\..</DockerfileContext>
<DockerComposeProjectPath>..\..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.ComponentModel;
using System.Reflection.Metadata.Ecma335;
namespace BlazorCanvas.Server.Components.Data; namespace BlazorCanvas.Server.Components.Data;
@@ -12,31 +14,45 @@ namespace BlazorCanvas.Server.Components.Data;
public class CanvasService { public class CanvasService {
private Canvas2DContext? _currentCanvasContext; private Canvas2DContext? _currentCanvasContext;
private IJSRuntime _jsRuntime;
private IRedisService _redisService; private IRedisService _redisService;
private CanvasCommand _lastCommand = new(); 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) { public CanvasService(IRedisService redisService) {
_jsRuntime = jsRuntime;
_redisService = redisService; _redisService = redisService;
} }
~CanvasService() {
_has_ended = true;
Task.Delay(100);
}
public string currentColor { get; set; } = "Black"; public string currentColor { get; set; } = "Black";
public int pointSize { get; set; } = 1; public int pointSize { get; set; } = 1;
public bool snap { get; set; } public bool snap { get; set; }
public ElementReference divCanvas { get; set; } public ElementReference divCanvas { get; set; }
public BECanvasComponent myCanvas { get; set; } = new(); public BECanvasComponent myCanvas { get; set; } = new();
/// <summary> public async void Consume() {
/// Version Pub/Sub while (!_has_ended) {
/// </summary> if (_lsComms.Count > 0)
public async void Subscribe() { _lsComms.Clear();
CancellationToken cToken = new(); _lsComms.AddRange(await _redisService.Consume());
while (!cToken.IsCancellationRequested) { if (_lsComms.Count == 1)
var comm = await _redisService.Subscribe(cToken); Draw(_lsComms[0]);
if (comm is not null) else if (_lsComms.Count > 0)
Draw(comm); 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.SetFillStyleAsync(command.Color);
await _currentCanvasContext.FillRectAsync(command.X, command.Y, command.PointSize, command.PointSize); await _currentCanvasContext.FillRectAsync(command.X, command.Y, command.PointSize, command.PointSize);
} }
try {
await _currentCanvasContext.EndBatchAsync(); await _currentCanvasContext.EndBatchAsync();
} catch { } // HAHAHAHA ÇA MARCHE.
} }
public async void Draw(CanvasCommand command) { public async void Draw(CanvasCommand command) {
@@ -71,33 +89,31 @@ public class CanvasService {
double mouseX = 0, mouseY = 0; double mouseX = 0, mouseY = 0;
if (!_is_started) { if (!_is_started) {
Subscribe(); if (!_is_init)
InitStreamer();
if (_is_init) {
Consume();
_is_started = true; _is_started = true;
} }
return;
}
if (eventArgs.Buttons == 0 || eventArgs.Buttons > 2) 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. return; // Rien faire si aucun bouton est appuyé ou si les deux boutons/ d'autres boutons sont appuyés.
if (divCanvas.Id?.Length > 0) { if (divCanvas.Id?.Length > 0) {
string data = await _jsRuntime.InvokeAsync<string>("getDivCanvasOffsets",
new object[] { divCanvas });
string color = "White"; string color = "White";
CanvasCommand command = new(); CanvasCommand command = new();
JObject? offsets = (JObject?)JsonConvert.DeserializeObject(data);
if (offsets is not null && offsets.HasValues) { // Translation entre le canvas et la souris. // Magnétisme boboche si activé, centrage si pas activé.
mouseX = eventArgs.PageX - offsets.Value<double>("offsetLeft"); mouseX = eventArgs.OffsetX;
mouseY = eventArgs.PageY - offsets.Value<double>("offsetTop"); 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 if (eventArgs.Buttons == 1) // Couleur si bouton gauche, blanc si bouton droit
color = currentColor; color = currentColor;
if (snap) { // Magnétisme boboche.
mouseX -= mouseX % pointSize;
mouseY -= mouseY % pointSize;
}
command.X = mouseX; command.X = mouseX;
command.Y = mouseY; command.Y = mouseY;
command.Color = color; command.Color = color;
@@ -106,9 +122,7 @@ public class CanvasService {
if (command.Equals(_lastCommand)) if (command.Equals(_lastCommand))
return; // Pour pas spammer des commandes si c'est pas pertinent. return; // Pour pas spammer des commandes si c'est pas pertinent.
_redisService.Publish(command); _redisService.Produce(command);
//Draw(command); // Local
_lastCommand = command; _lastCommand = command;
} }

View File

@@ -1,6 +1,12 @@
namespace BlazorCanvas.Server.Components.Data; namespace BlazorCanvas.Server.Components.Data;
public interface IRedisService { public interface IRedisService {
public Task<CanvasCommand?> Subscribe(CancellationToken cToken); Task<bool> InitStreamer();
public void Publish(CanvasCommand command); bool InitSubscriber();
Task<CanvasCommand?> Subscribe(CancellationToken cToken);
void Publish(CanvasCommand command);
void Produce(CanvasCommand command);
Task<IEnumerable<CanvasCommand>> Consume();
} }

View File

@@ -1,17 +1,85 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StackExchange.Redis; using StackExchange.Redis;
using System;
using System.Reflection.Metadata.Ecma335;
namespace BlazorCanvas.Server.Components.Data; namespace BlazorCanvas.Server.Components.Data;
// https://developer.redis.com/develop/dotnet/streams/stream-basics/ // 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 IConnectionMultiplexer _cache;
private IDatabase _database;
private ChannelMessageQueue _channel; private ChannelMessageQueue _channel;
public RedisService(IConnectionMultiplexer cache) { public RedisService(IConnectionMultiplexer cache) { _cache = cache; }
_cache = cache;
_channel = _cache.GetSubscriber().Subscribe(RedisChannel.Literal("lol")); /// <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> /// <summary>
@@ -20,10 +88,13 @@ public class RedisService: IRedisService {
/// </summary> /// </summary>
public async Task<CanvasCommand?> Subscribe(CancellationToken cToken) { public async Task<CanvasCommand?> Subscribe(CancellationToken cToken) {
var mess = await _channel.ReadAsync(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) if (comm is not null)
return comm; return comm;
else return null; return null;
} }
/// <summary> /// <summary>
@@ -32,7 +103,6 @@ public class RedisService: IRedisService {
/// </summary> /// </summary>
/// <param name="command">La commande à publier</param> /// <param name="command">La commande à publier</param>
public async void Publish(CanvasCommand command) { public async void Publish(CanvasCommand command) {
CanvasCommand cm = new(command); await _cache.GetSubscriber().PublishAsync(_channel.Channel, JsonConvert.SerializeObject(command));
await _cache.GetSubscriber().PublishAsync(_channel.Channel, JsonConvert.SerializeObject(cm));
} }
} }

View 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

View File

@@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"ConnectionStrings": {
"cache": "francis-redis"
}
} }

View File

@@ -1,2 +1,20 @@
# Blazor_Canvas [![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-24ddc0f5d75046c5622901739e7c5dd533143b0c8e959d652212380cedb1ea36.svg)](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 jusquau 20 décembre pour assembler un MVP (Minimum Viable Product), et sil est assez impressionné il vous engagera comme CTO (Chief Technical Officer) pour une startup quil fondera à ce moment-là.
## SPÉCIFICATIONS :
Afin de pouvoir démontrer que votre serveur puisse être mis en grappe, la seule contrainte de lapplication sera quau minimum 2 utilisateurs puissent interagir ensemble. Ainsi, si un des utilisateurs se connecte à 1 nœud de la grappe et quun autre se connecte sur le deuxième nœud, les deux devraient pouvoir utiliser les fonctionnalités comme sil ny avait quun 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 quun 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 dinteragir avec votre serveur à laide dun interface utilisateur (UI), mais il nest pas nécessaire que linterface 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 dajouter 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