remove FirmwareCiDaemon.csproj
This commit is contained in:
parent
45bd531206
commit
95048e42d6
|
@ -1,17 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../InnovEnergy.app.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>InnovEnergy.Server.FirmwareCiDaemon</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Reactive.Linq" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../lib/Utils/Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
|
@ -1,21 +0,0 @@
|
|||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public enum Branch
|
||||
{
|
||||
Release,
|
||||
Develop,
|
||||
Victron,
|
||||
}
|
||||
|
||||
public static class BranchExtensions
|
||||
{
|
||||
public static String GetName(this Branch branch) => branch switch
|
||||
{
|
||||
Branch.Victron => "venus.victron",
|
||||
Branch.Release => "venus.release",
|
||||
Branch.Develop => "venus",
|
||||
_ => throw new Exception($"Unsupported branch: {branch}")
|
||||
};
|
||||
|
||||
|
||||
}
|
|
@ -1,295 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using CliWrap;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.Logger;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.ExitException;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public record Build
|
||||
{
|
||||
private static readonly Regex RxTimestamp = new Regex(@"\d{14}");
|
||||
private static readonly Regex RxVersion = new Regex(@"v\d+\.\d+(~\w+)?");
|
||||
|
||||
private const Int32 MaxPublishedSwus = 12;
|
||||
|
||||
public Device Device { get; init; }
|
||||
public String VeVersion { get; init; }
|
||||
public Branch Branch { get; init; }
|
||||
public String Comment { get; init; }
|
||||
public String Hash { get; init; }
|
||||
|
||||
public String ShortHash => Hash[..6];
|
||||
|
||||
public Channel Channel => Branch switch
|
||||
{
|
||||
Branch.Victron => Channel.Testing,
|
||||
Branch.Release => Channel.Release,
|
||||
Branch.Develop => Channel.Develop,
|
||||
_ => throw new Exception($"Unsupported branch: {Branch}")
|
||||
};
|
||||
|
||||
public String VersionId => Branch switch
|
||||
{
|
||||
Branch.Victron => $"{VeVersion}~victron",
|
||||
Branch.Release => $"{VeVersion}~{Comment}",
|
||||
Branch.Develop => $"{VeVersion}~{ShortHash}",
|
||||
_ => throw new Exception($"Unsupported branch: {Branch}")
|
||||
};
|
||||
|
||||
public static Build Prepare(CommitInfo ie, (Device device, String veVersion) ve)
|
||||
{
|
||||
return new Build
|
||||
{
|
||||
Device = ve.device,
|
||||
VeVersion = ve.veVersion,
|
||||
Branch = ie.Branch,
|
||||
Comment = ie.Comment,
|
||||
Hash = ie.Hash
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
Log();
|
||||
Log("======================================================");
|
||||
Log("Starting build");
|
||||
Log("======================================================");
|
||||
Log();
|
||||
Log($"VE Version: {VeVersion}");
|
||||
Log($"Channel : {Channel}");
|
||||
Log($"Device : {Device}");
|
||||
Log($"Branch : {Branch}");
|
||||
Log($"Comment : {Comment}");
|
||||
Log($"Version ID: {VersionId}");
|
||||
Log($"SWU Name : {Device.SwuName()}");
|
||||
Log($"Hash : {Hash}");
|
||||
Log();
|
||||
Log("======================================================");
|
||||
Log();
|
||||
|
||||
using var veBaseSwuFile = FwSource.Victron.GetLatestSwuPath(Channel.Release, Device).Apply(DownloadFile);
|
||||
using var ieRepo = Fossil.Checkout(Branch);
|
||||
using var releaseSwu = MergeSwu(veBaseSwuFile, ieRepo.FirmwareDirectory, Device, VersionId);
|
||||
|
||||
var publishedSwu = PublishSwu(Device, Channel, releaseSwu);
|
||||
|
||||
RemoveOldFiles(Device, Channel);
|
||||
|
||||
var zipPath = Channel.ZipPath();
|
||||
var removeGlob = Device.SwuBase() + "*";
|
||||
|
||||
if (zipPath is null)
|
||||
return;
|
||||
|
||||
Log($"Updating {zipPath[Program.IeBasePath.Length..]}");
|
||||
|
||||
zipPath.RemoveFromZip(removeGlob);
|
||||
zipPath.AddToZip(publishedSwu);
|
||||
}
|
||||
|
||||
public static Disposable<String> DownloadFile(String url)
|
||||
{
|
||||
var fileName = Path.GetFileName(url);
|
||||
|
||||
var downloadedFile = FileSystem.CreateTempFile(fileName);
|
||||
|
||||
Log($"downloading {url}");
|
||||
|
||||
var curl = Cli
|
||||
.Wrap("curl")
|
||||
.WithArguments(url)
|
||||
.PipeToFile(downloadedFile);
|
||||
|
||||
if (curl.exitCode != 0)
|
||||
Exit("Failed to download " + url);
|
||||
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
private static void UpdateVersionFile(String fwDir, String version, String timestamp, String file)
|
||||
{
|
||||
Log($"updating {file}");
|
||||
|
||||
var fwFile = fwDir.AppendPath(file);
|
||||
|
||||
if (!FileSystem.Local.FileExists(fwFile))
|
||||
Exit($"Cannot find {fwFile}");
|
||||
|
||||
var contents = File.ReadAllText(fwFile);
|
||||
|
||||
contents = RxTimestamp.Replace(contents, timestamp);
|
||||
contents = RxVersion.Replace(contents, version);
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fwFile, contents);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Exit($"Failed to write to {fwFile}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PatchIeFiles(String fossilDir, String mountDir)
|
||||
{
|
||||
Log("applying changes");
|
||||
|
||||
var (exitCode, stdOut, stdErr) = Cli
|
||||
.Wrap("rsync")
|
||||
.WithArguments($"-r -t -v -l -i -u -I {fossilDir}/ {mountDir}") // that / is important!
|
||||
.ExecuteSync();
|
||||
|
||||
if (exitCode != 0)
|
||||
Exit($"Failed to apply changes!\n{stdErr}\n{stdOut}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Disposable<String> Unzip(String ext4GzFile)
|
||||
{
|
||||
var ext4GzFileName = Path.GetFileName(ext4GzFile);
|
||||
Log($"extracting {ext4GzFileName}");
|
||||
|
||||
var ext4File = ext4GzFileName
|
||||
.RemoveSuffix(".gz")
|
||||
.Apply(FileSystem.CreateTempFile);
|
||||
|
||||
var zcat = Cli
|
||||
.Wrap("zcat")
|
||||
.WithArguments(ext4GzFile)
|
||||
.PipeToFile(ext4File);
|
||||
|
||||
if (zcat.exitCode != 0)
|
||||
{
|
||||
ext4File.Dispose();
|
||||
Exit("Failed to extract " + ext4GzFile);
|
||||
}
|
||||
|
||||
return ext4File;
|
||||
}
|
||||
|
||||
private static Disposable<String> Mount(String ext4File)
|
||||
{
|
||||
var ext4FileName = Path.GetFileName(ext4File);
|
||||
Log($"mounting {ext4FileName}");
|
||||
|
||||
var mountDir = FileSystem.CreateTmpDir();
|
||||
|
||||
var mount = Cli
|
||||
.Wrap("mount")
|
||||
.WithArguments($"-o loop -t ext4 {ext4File} {mountDir}")
|
||||
.ExecuteSync();
|
||||
|
||||
if (mount.exitCode != 0)
|
||||
{
|
||||
Log($"\nFailed to mount {ext4File} on {mountDir}:\n{mount.stdErr}");
|
||||
|
||||
mountDir.Dispose();
|
||||
Exit($"\nFailed to mount {ext4File}:\n{mount.stdErr}");
|
||||
}
|
||||
|
||||
return mountDir.BeforeDisposeDo(Unmount);
|
||||
|
||||
void Unmount()
|
||||
{
|
||||
Log($"unmounting {ext4FileName}");
|
||||
|
||||
var umount = Cli
|
||||
.Wrap("umount")
|
||||
.WithArguments(mountDir)
|
||||
.ExecuteSync();
|
||||
|
||||
if (umount.exitCode != 0)
|
||||
Exit($"Failed to unmount {mountDir}\n{umount.stdErr}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyChanges(String ieFirmwareDir,
|
||||
String ext4File,
|
||||
String timestamp,
|
||||
String version)
|
||||
{
|
||||
using var mountDir = Mount(ext4File); // IMPORTANT: must unmount (dispose) before zipping ext4 file again!
|
||||
|
||||
PatchIeFiles(ieFirmwareDir, mountDir);
|
||||
|
||||
UpdateVersionFile(mountDir, version, timestamp, "/etc/issue");
|
||||
UpdateVersionFile(mountDir, version, timestamp, "/etc/issue.net");
|
||||
UpdateVersionFile(mountDir, version, timestamp, "/etc/version");
|
||||
UpdateVersionFile(mountDir, version, timestamp, "/opt/victronenergy/version");
|
||||
}
|
||||
|
||||
|
||||
private static void UpdateSymlink(String newSwuFile, String newSwuLink)
|
||||
{
|
||||
var ln = Cli
|
||||
.Wrap("ln")
|
||||
.WithArguments($"-sfn {newSwuFile} {newSwuLink}")
|
||||
.ExecuteSync();
|
||||
|
||||
if (ln.exitCode != 0)
|
||||
Exit($"failed to update symlink {newSwuLink}");
|
||||
}
|
||||
|
||||
|
||||
private static void RemoveOldFiles(Device device, Channel channel)
|
||||
{
|
||||
var oldFiles = Directory
|
||||
.GetFiles(FwSource.InnovEnergy.GetDirectory(channel, device))
|
||||
.Where(f => f.EndsWith(".swu"))
|
||||
.OrderByDescending(File.GetCreationTimeUtc)
|
||||
.Skip(MaxPublishedSwus);
|
||||
|
||||
foreach (var file in oldFiles)
|
||||
{
|
||||
Log($"Deleting old swu file {file.Substring(Program.IeBasePath.Length)}");
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
private static String PublishSwu(Device device, Channel channel, String releaseSwu)
|
||||
{
|
||||
var swuFileName = Path.GetFileName(releaseSwu);
|
||||
var newSwuFile = FwSource.InnovEnergy.GetDirectory(channel, device).AppendPath(swuFileName);
|
||||
var newSwuLink = FwSource.InnovEnergy.GetLatestSwuPath(channel, device);
|
||||
|
||||
Log($"publishing {newSwuFile[Program.IeBasePath.Length..]}");
|
||||
|
||||
new FileInfo(newSwuFile).Directory?.Create(); // create dir if not exits
|
||||
File.Move(releaseSwu, newSwuFile);
|
||||
|
||||
UpdateSymlink(newSwuFile, newSwuLink);
|
||||
|
||||
return newSwuFile;
|
||||
}
|
||||
|
||||
|
||||
private static Disposable<String> MergeSwu(String victronBaseSwuFile,
|
||||
String ieFirmwareDir,
|
||||
Device device,
|
||||
String version)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
|
||||
|
||||
using var cpioDir = Cpio.Extract(victronBaseSwuFile);
|
||||
|
||||
var ext4GzFile = cpioDir.Value.AppendPath(device.ExtGzFileName());
|
||||
|
||||
using var ext4File = Unzip(ext4GzFile);
|
||||
|
||||
UpdateVersionFile(cpioDir, version, timestamp, "/sw-description");
|
||||
ApplyChanges(ieFirmwareDir, ext4File, timestamp, version);
|
||||
|
||||
Zip.GZip(ext4File, ext4GzFile);
|
||||
|
||||
var mergedSwuFile = device
|
||||
.LongSwuFileName(timestamp, version)
|
||||
.Apply(FileSystem.CreateTempFile);
|
||||
|
||||
Cpio.Write(cpioDir, mergedSwuFile);
|
||||
|
||||
return mergedSwuFile;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public enum Channel
|
||||
{
|
||||
Release,
|
||||
Candidate,
|
||||
Testing,
|
||||
Develop
|
||||
}
|
||||
|
||||
public static class ChannelExtensions
|
||||
{
|
||||
public static String GetName(this Channel channel)
|
||||
{
|
||||
return Enum.GetName(typeof(Channel), channel)?.ToLower()!;
|
||||
}
|
||||
|
||||
public static String? ZipPath(this Channel channel)
|
||||
{
|
||||
if (channel is Channel.Candidate or Channel.Develop)
|
||||
return null;
|
||||
|
||||
var fileName = channel is Channel.Release
|
||||
? "release.zip"
|
||||
: "victron.zip";
|
||||
|
||||
return Program.IeBasePath.AppendPath("venus").AppendPath(fileName);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
using System.Text;
|
||||
using CliWrap;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public static class CliExtensions
|
||||
{
|
||||
|
||||
// TODO: obsolete, use ExecuteBufferedAsync
|
||||
public static (Int32 exitCode, String stdOut, String stdErr) ExecuteSync(this Command cmd)
|
||||
{
|
||||
var stdErr = new StringBuilder();
|
||||
var stdOut = new StringBuilder();
|
||||
|
||||
var r = cmd
|
||||
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOut))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErr))
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync()
|
||||
.Task
|
||||
.Result;
|
||||
|
||||
return (r.ExitCode, stdOut.ToString(), stdErr.ToString());
|
||||
}
|
||||
|
||||
public static (Int32 exitCode, String stdErr) PipeToFile(this Command cmd, String file)
|
||||
{
|
||||
var stdErr = new StringBuilder();
|
||||
|
||||
var r = cmd
|
||||
.WithStandardOutputPipe(PipeTarget.ToFile(file))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErr))
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync()
|
||||
.Task
|
||||
.Result;
|
||||
|
||||
return (r.ExitCode, stdErr.ToString());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public class Commit : Disposable
|
||||
{
|
||||
public String Directory { get; }
|
||||
public String FirmwareDirectory => Directory.AppendPath("firmware");
|
||||
|
||||
public Commit(Disposable<String> directory) : base(directory.Dispose)
|
||||
{
|
||||
Directory = directory;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public record CommitInfo
|
||||
{
|
||||
public String Hash { get; init; }
|
||||
public String Comment { get; init; }
|
||||
public Branch Branch { get; init; }
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
using CliWrap;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.ExitException;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.Logger;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public static class Cpio
|
||||
{
|
||||
public static void Write(String cpioDir, String swuFile)
|
||||
{
|
||||
var swuFileName = Path.GetFileName(swuFile);
|
||||
|
||||
Log($"writing {swuFileName}");
|
||||
|
||||
// ls | cpio -o -H crc > "../$swu" 2>/dev/null
|
||||
|
||||
var ls = Cli
|
||||
.Wrap("ls")
|
||||
.WithWorkingDirectory(cpioDir);
|
||||
|
||||
var cpio = Cli
|
||||
.Wrap("cpio")
|
||||
.WithArguments("-o -H crc")
|
||||
.WithStandardOutputPipe(PipeTarget.ToFile(swuFile))
|
||||
.WithWorkingDirectory(cpioDir);
|
||||
|
||||
var lsCpio = (ls | cpio).ExecuteAsync().Task.Result;
|
||||
|
||||
if (lsCpio.ExitCode != 0)
|
||||
Exit($"Failed to write {swuFileName}");
|
||||
}
|
||||
|
||||
public static Disposable<String> Extract(String swuFile)
|
||||
{
|
||||
Log($"extracting {Path.GetFileName(swuFile)}");
|
||||
|
||||
var cpioDir = FileSystem.CreateTmpDir();
|
||||
|
||||
var cpio = Cli
|
||||
.Wrap("cpio")
|
||||
.WithArguments("-id")
|
||||
.WithStandardInputPipe(PipeSource.FromFile(swuFile))
|
||||
.WithWorkingDirectory(cpioDir)
|
||||
.ExecuteSync();
|
||||
|
||||
if (cpio.exitCode != 0)
|
||||
{
|
||||
cpioDir.Dispose();
|
||||
Exit("Failed to extract swu");
|
||||
}
|
||||
|
||||
return cpioDir;
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public enum Device
|
||||
{
|
||||
BeagleBone,
|
||||
BeagleBone2,
|
||||
NanoPi,
|
||||
Einstein,
|
||||
RaspberryPi2
|
||||
}
|
||||
|
||||
public static class DeviceExtensions
|
||||
{
|
||||
public static String GetName(this Device device)
|
||||
{
|
||||
if (device == Device.BeagleBone2)
|
||||
device = Device.BeagleBone;
|
||||
|
||||
var name = Enum.GetName(device)?.ToLower();
|
||||
if (name is null)
|
||||
throw new ArgumentException(nameof(device));
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public static String SwuName(this Device device)
|
||||
{
|
||||
return device switch
|
||||
{
|
||||
Device.BeagleBone2 => "venus-swu-2",
|
||||
_ => "venus-swu"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static String SwuBase(this Device device)
|
||||
{
|
||||
var deviceName = device.GetName();
|
||||
var swuName = device.SwuName();
|
||||
return $"{swuName}-{deviceName}";
|
||||
}
|
||||
|
||||
public static String ShortSwuFileName(this Device device)
|
||||
{
|
||||
var swuBase = device.SwuBase();
|
||||
return $"{swuBase}.swu";
|
||||
}
|
||||
|
||||
public static String ExtFileName(this Device device)
|
||||
{
|
||||
var deviceName = device.GetName();
|
||||
return $"venus-image-{deviceName}.ext4";
|
||||
}
|
||||
|
||||
public static String ExtGzFileName(this Device device)
|
||||
{
|
||||
return device.ExtFileName() + ".gz";
|
||||
}
|
||||
|
||||
public static String LongSwuFileName(this Device device, String timestamp, String version)
|
||||
{
|
||||
var swuBase = device.SwuBase();
|
||||
return $"{swuBase}-{timestamp}-{version}.swu";
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public class ExitException : Exception
|
||||
{
|
||||
public Int32 ExitCode { get; }
|
||||
|
||||
public ExitException(Int32 exitCode = 1): base("")
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
|
||||
public ExitException(String message, Int32 exitCode = 1) : base(message)
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
|
||||
public static void Exit(String msg) => throw new ExitException(msg);
|
||||
public static void Exit(Int32 exitCode) => throw new ExitException(exitCode);
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
using CliWrap;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static System.StringSplitOptions;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.ExitException;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public static class Fossil
|
||||
{
|
||||
public static String FossilFile { get; set; }
|
||||
public static String FossilUser { get; set; }
|
||||
|
||||
// TODO: get commit/branch without checkout
|
||||
|
||||
public static Commit Checkout(this Branch branch)
|
||||
{
|
||||
var fossilDir = Open();
|
||||
Update(fossilDir, branch, FossilUser);
|
||||
return new Commit(fossilDir);
|
||||
}
|
||||
|
||||
public static CommitInfo GetLatestCommitInfo(this Branch branch)
|
||||
{
|
||||
using var fossilDir = Open();
|
||||
var (hash, comment) = Update(fossilDir, branch, FossilUser);
|
||||
return new CommitInfo { Hash = hash, Comment = comment, Branch = branch};
|
||||
}
|
||||
|
||||
private static Disposable<String> Open()
|
||||
{
|
||||
var fossilDir = FileSystem.CreateTmpDir();
|
||||
var fossilCmd = Cli
|
||||
.Wrap("fossil")
|
||||
.WithWorkingDirectory(fossilDir);
|
||||
|
||||
var open = fossilCmd
|
||||
.WithArguments($"open {FossilFile} --empty")
|
||||
.ExecuteSync();
|
||||
|
||||
if (open.exitCode != 0)
|
||||
{
|
||||
fossilDir.Dispose();
|
||||
Exit("failed to open " + Path.GetFileName(FossilFile) + "\n" + open.stdOut + "\n" + open.stdErr);
|
||||
}
|
||||
|
||||
return fossilDir.BeforeDisposeDo(Close);
|
||||
|
||||
void Close() => fossilCmd.WithArguments("close").ExecuteSync();
|
||||
}
|
||||
|
||||
public static (String hash, String comment) Update(String fossilDir, Branch branch, String fossilUser)
|
||||
{
|
||||
var fossil = Cli
|
||||
.Wrap("fossil")
|
||||
.WithArguments($"update --user {fossilUser} {branch.GetName()}")
|
||||
.WithWorkingDirectory(fossilDir)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteSync();
|
||||
|
||||
if (fossil.exitCode != 0)
|
||||
Exit($"failed to update!\n{fossil.stdOut}");
|
||||
|
||||
var fossilOutLines = fossil.stdOut.SplitLines();
|
||||
|
||||
const String checkoutPrefix = "updated-to:";
|
||||
const String commentPrefix = "comment:";
|
||||
|
||||
var hash = fossilOutLines
|
||||
.Single(l => l.StartsWith(checkoutPrefix))
|
||||
.Split(" ", RemoveEmptyEntries)
|
||||
.ElementAt(1);
|
||||
|
||||
var comment = fossilOutLines
|
||||
.Single(l => l.StartsWith(commentPrefix))
|
||||
.Substring(commentPrefix.Length)
|
||||
.SkipWhile(c => c == ' ')
|
||||
.TakeWhile(c => c != '(') // there is a (user: ig) at the end, stops at the first (, but whatever
|
||||
.Apply(String.Concat)
|
||||
.Trim();
|
||||
|
||||
return (hash, comment);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using InnovEnergy.Lib.Utils;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public enum FwSource
|
||||
{
|
||||
InnovEnergy,
|
||||
Victron
|
||||
}
|
||||
|
||||
public static class FwSourceExtensions
|
||||
{
|
||||
public static String GetLatestSwuPath(this FwSource source, Channel channel, Device device)
|
||||
{
|
||||
var path = source.GetDirectory(channel, device);
|
||||
var file = device.ShortSwuFileName();
|
||||
|
||||
return path.AppendPath(file);
|
||||
}
|
||||
|
||||
public static String GetDirectory(this FwSource source, Channel channel, Device device)
|
||||
{
|
||||
var basePath = source == FwSource.InnovEnergy
|
||||
? Program.IeBasePath
|
||||
: Program.VeBasePath;
|
||||
|
||||
return basePath
|
||||
.AppendPath("venus")
|
||||
.AppendPath(channel.GetName())
|
||||
.AppendPath("images")
|
||||
.AppendPath(device.GetName()) + '/';
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public static class Logger
|
||||
{
|
||||
public static void Log(String s = "")
|
||||
{
|
||||
Console.WriteLine($"{DateTime.Now:yyyy'-'MM'-'dd' 'HH':'mm':'ss} {s}");
|
||||
}
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text.RegularExpressions;
|
||||
using CliWrap;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.Logger;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.ExitException;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
// dotnet publish FirmwareCiDaemon.csproj -c Release -r linux-x64 -p:PublishSingleFile=true --self-contained true && scp ./bin/Release/netcoreapp5.0/linux-x64/publish/FirmwareCiDaemon ig@salidomo.innovenergy.ch:/home/ig/firmware
|
||||
|
||||
public static class Program
|
||||
{
|
||||
|
||||
// use the following scheduler to run everything on a single thread
|
||||
// Fossil cannot deal with the async introduced by multiple threads/tasks
|
||||
private static readonly IScheduler Scheduler = new EventLoopScheduler();//NewThreadScheduler();
|
||||
|
||||
private static readonly IEnumerable<Device> SupportedDevices = Enum.GetValues<Device>();
|
||||
private static readonly IEnumerable<Branch> Branches = Enum.GetValues<Branch>();
|
||||
|
||||
public static String IeBasePath { get; set; }
|
||||
public static String VeBasePath => "https://updates.victronenergy.com/feeds/";
|
||||
|
||||
public static void Main(String[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Run(args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PrintExceptionAndExit(e);
|
||||
}
|
||||
//Environment.Exit(0);
|
||||
}
|
||||
|
||||
// runcmd: program <fossil_file> <iePublishDir> <fossil_user>
|
||||
|
||||
private static void Run(String[] args)
|
||||
{
|
||||
if (!IsRoot) Exit("This program must be run as root");
|
||||
|
||||
Fossil.FossilFile = ParseFossilFileArg(args); // TODO: get rid of global vars?
|
||||
Fossil.FossilUser = ParseFossilUserArg(args);
|
||||
IeBasePath = ParseIePublishDirArg(args);
|
||||
|
||||
var veVersions = SupportedDevices.Select(ObserveVeVersion).ToList();
|
||||
var branches = Branches.Select(ObserveFossilBranch).ToList();
|
||||
|
||||
var buildInfos = from veVersion in veVersions
|
||||
from branch in branches
|
||||
select Observable.CombineLatest(branch, veVersion, Build.Prepare);
|
||||
|
||||
buildInfos.Merge()
|
||||
.ObserveOn(Scheduler) // fossil is not thread safe!
|
||||
.Subscribe(build => build.Execute(), PrintExceptionAndExit);
|
||||
|
||||
foreach (var veVersion in veVersions)
|
||||
veVersion.Connect();
|
||||
|
||||
foreach (var branch in branches)
|
||||
branch.Connect();
|
||||
|
||||
while (true) Console.ReadLine();
|
||||
|
||||
// ReSharper disable once FunctionNeverReturns
|
||||
}
|
||||
|
||||
private static IConnectableObservable<CommitInfo> ObserveFossilBranch(Branch branch)
|
||||
{
|
||||
return Observable
|
||||
.Interval(TimeSpan.FromMinutes(1))
|
||||
.StartWith(0)
|
||||
.ObserveOn(Scheduler) // fossil is not thread safe!
|
||||
.Select(_ => Fossil.GetLatestCommitInfo(branch))
|
||||
.DistinctUntilChanged()
|
||||
.Do(c => Log($"Found new commit '{c.Comment}' on branch {c.Branch} ({c.Hash[..6]})"))
|
||||
.Publish();
|
||||
}
|
||||
|
||||
private static IConnectableObservable<(Device device, String veVersion)> ObserveVeVersion(Device device)
|
||||
{
|
||||
return Observable
|
||||
.Interval(TimeSpan.FromHours(1))
|
||||
.StartWith(0)
|
||||
.Select(_ => device)
|
||||
.Select(GetLatestVictronVersion)
|
||||
.DistinctUntilChanged()
|
||||
.Select(veVersion => (device, veVersion))
|
||||
.Do(vd => Log($"Found new Victron version '{vd.veVersion}' for {vd.device}"))
|
||||
.Publish();
|
||||
}
|
||||
|
||||
public static String GetLatestVictronVersion(Device device)
|
||||
{
|
||||
var url = FwSource.Victron.GetDirectory(Channel.Release, device);
|
||||
|
||||
var curl = Cli
|
||||
.Wrap("curl")
|
||||
.WithArguments(url)
|
||||
.ExecuteSync()
|
||||
.stdOut;
|
||||
|
||||
var deviceName = device.GetName();
|
||||
var swuName = device.SwuName();
|
||||
|
||||
const String timeStampPattern = "[0-9]{14}";
|
||||
const String releasePattern = @"v[0-9]+\.[0-9]+";
|
||||
const String extension = ".swu";
|
||||
const String extensionPattern = @"\" + extension; // escape .
|
||||
|
||||
var rxVersion = new Regex($"{swuName}-{deviceName}-{timeStampPattern}-{releasePattern}{extensionPattern}");
|
||||
|
||||
return rxVersion
|
||||
.Matches(curl)
|
||||
.Select(m => m.Value)
|
||||
.OrderBy(m => m)
|
||||
.Last()
|
||||
.Split("-")
|
||||
.Last()
|
||||
.RemoveSuffix(extension);
|
||||
}
|
||||
|
||||
private static Boolean IsRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
var (exitCode, stdOut, stdErr) = Cli
|
||||
.Wrap("id")
|
||||
.WithArguments("-u")
|
||||
.ExecuteSync();
|
||||
|
||||
return exitCode == 0 &&
|
||||
stdOut.Trim() == "0" &&
|
||||
String.IsNullOrWhiteSpace(stdErr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static String ParseFossilFileArg(IReadOnlyList<String> args)
|
||||
{
|
||||
if (args.Count < 1)
|
||||
Exit("Missing argument <fossil_file>");
|
||||
|
||||
var fossilFile = Path.GetFullPath(args[0]);
|
||||
|
||||
if (!FileSystem.Local.FileExists(fossilFile))
|
||||
Exit($"Cannot find fossil file {fossilFile}");
|
||||
|
||||
return fossilFile;
|
||||
}
|
||||
|
||||
private static String ParseIePublishDirArg(IReadOnlyList<String> args)
|
||||
{
|
||||
if (args.Count < 2)
|
||||
Exit("Missing argument <iePublishDir>");
|
||||
|
||||
var iePublishDir = Path.GetFullPath(args[1]);
|
||||
|
||||
if (!FileSystem.Local.DirectoryExists(iePublishDir))
|
||||
Exit($"Cannot find directory {iePublishDir}");
|
||||
|
||||
return iePublishDir;
|
||||
}
|
||||
|
||||
private static String ParseFossilUserArg(IReadOnlyList<String> args)
|
||||
{
|
||||
if (args.Count < 3)
|
||||
Exit("Missing argument <fossil_user>");
|
||||
|
||||
return args[2];
|
||||
}
|
||||
|
||||
private static void PrintExceptionAndExit(Exception e)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(e.Message))
|
||||
{
|
||||
Console.WriteLine();
|
||||
|
||||
var color = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Log(e.Message);
|
||||
Console.ForegroundColor = color;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(e.StackTrace))
|
||||
Log(e.StackTrace);
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
using CliWrap;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.Logger;
|
||||
using static InnovEnergy.Server.FirmwareCiDaemon.ExitException;
|
||||
|
||||
namespace InnovEnergy.Server.FirmwareCiDaemon;
|
||||
|
||||
public static class Zip
|
||||
{
|
||||
public static void RemoveFromZip(this String zipFilePath, String zipInternalPathGlob)
|
||||
{
|
||||
var zip = Cli
|
||||
.Wrap("zip")
|
||||
.WithArguments($"-q -d {zipFilePath} {zipInternalPathGlob}")
|
||||
.ExecuteSync();
|
||||
|
||||
if (zip.exitCode != 0)
|
||||
Log($"Warning: failed to remove {zipInternalPathGlob} from {zipFilePath}");
|
||||
}
|
||||
|
||||
public static void AddToZip(this String zipFilePath, String filePath)
|
||||
{
|
||||
var zip = Cli
|
||||
.Wrap("zip")
|
||||
.WithArguments($"-q -j {zipFilePath} {filePath}")
|
||||
.ExecuteSync();
|
||||
|
||||
if (zip.exitCode != 0)
|
||||
Exit($"failed to add {filePath} to {zipFilePath}");
|
||||
}
|
||||
|
||||
public static void GZip(this String sourceFile, String targetGzFile)
|
||||
{
|
||||
// TODO: datetime log
|
||||
|
||||
Log($"compressing {Path.GetFileName(sourceFile)}");
|
||||
|
||||
var gzip = Cli
|
||||
.Wrap("gzip")
|
||||
.WithArguments($"-c {sourceFile}")
|
||||
.PipeToFile(targetGzFile);
|
||||
|
||||
if (gzip.exitCode != 0)
|
||||
Exit($"Failed to gzip {sourceFile}");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue