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