1 module dls.updater;
2 
3 import dls.bootstrap : repoBase;
4 import dls.protocol.interfaces : InitializeParams;
5 import dub.dub : Dub;
6 import std.file : FileException;
7 import std.format : format;
8 import std.json : parseJSON;
9 
10 private enum descriptionJson = import("description.json");
11 private immutable changelogUrl = format!"https://github.com/%s/dls/blob/master/CHANGELOG.md"(
12         repoBase);
13 
14 void cleanup()
15 {
16     import dls.bootstrap : dubBinDir;
17     import dub.package_ : Package;
18     import std.file : SpanMode, dirEntries, remove, rmdirRecurse;
19     import std.path : baseName;
20     import std.regex : matchFirst;
21 
22     auto dub = new Dub();
23     Package[] packagesToRemove;
24 
25     foreach (dlsPackage; dub.packageManager.getPackageIterator("dls"))
26     {
27         if (dlsPackage.version_.toString() < currentVersion)
28         {
29             packagesToRemove ~= dlsPackage;
30         }
31     }
32 
33     foreach (dlsPackage; packagesToRemove)
34     {
35         try
36         {
37             dub.remove(dlsPackage);
38         }
39         catch (FileException e)
40         {
41         }
42     }
43 
44     bool[string] entriesToRemove;
45 
46     foreach (entry; dirEntries(dubBinDir, SpanMode.shallow))
47     {
48         const match = entry.name.baseName.matchFirst(`dls-v([\d.]+)`);
49 
50         if (match)
51         {
52             if (match[1] < currentVersion)
53             {
54                 foreach (subEntry; dirEntries(entry.name, SpanMode.shallow))
55                 {
56                     if (subEntry.baseName !in entriesToRemove)
57                     {
58                         entriesToRemove[subEntry.name.baseName] = true;
59                     }
60                 }
61 
62                 try
63                 {
64                     rmdirRecurse(entry.name);
65                 }
66                 catch (FileException e)
67                 {
68                 }
69             }
70             else
71             {
72                 foreach (subEntry; dirEntries(entry.name, SpanMode.shallow))
73                 {
74                     entriesToRemove[subEntry.name.baseName] = false;
75                 }
76             }
77         }
78     }
79 
80     foreach (entry; dirEntries(dubBinDir, SpanMode.shallow))
81     {
82         if (entry.name.baseName in entriesToRemove && entriesToRemove[entry.name.baseName])
83         {
84             try
85             {
86                 remove(entry.name);
87             }
88             catch (FileException e)
89             {
90             }
91         }
92     }
93 }
94 
95 @trusted void update()
96 {
97     import core.time : hours;
98     import dls.bootstrap : UpgradeFailedException, apiEndpoint, buildDls,
99         canDownloadDls, downloadDls, linkDls;
100     static import dls.protocol.jsonrpc;
101     import dls.protocol.messages.window : Util;
102     import dls.util.logger : logger;
103     import dls.util.path : normalized;
104     import dub.dependency : Dependency;
105     import dub.dub : FetchOptions;
106     import std.concurrency : ownerTid, receiveOnly, register, send, thisTid;
107     import std.datetime : Clock, SysTime;
108     import std.net.curl : get;
109 
110     const latestRelease = parseJSON(get(format!apiEndpoint("releases/latest")));
111     const latestVersion = latestRelease["tag_name"].str;
112     const releaseDate = SysTime.fromISOExtString(latestRelease["published_at"].str);
113 
114     if (latestVersion.length == 0 || ('v' ~ currentVersion) >= latestVersion
115             || (Clock.currTime - releaseDate < 1.hours))
116     {
117         return;
118     }
119 
120     auto id = Util.sendMessageRequest(Util.ShowMessageRequestType.upgradeDls,
121             [latestVersion, ('v' ~ currentVersion)]);
122     const threadName = "updater";
123     register(threadName, thisTid());
124     send(ownerTid(), Util.ThreadMessageData(id,
125             Util.ShowMessageRequestType.upgradeDls, threadName));
126 
127     const shouldUpgrade = receiveOnly!bool();
128 
129     if (!shouldUpgrade)
130     {
131         return;
132     }
133 
134     dls.protocol.jsonrpc.send("$/dls.upgradeDls.start");
135 
136     scope (exit)
137     {
138         dls.protocol.jsonrpc.send("$/dls.upgradeDls.stop");
139     }
140 
141     bool success;
142 
143     if (canDownloadDls)
144     {
145         try
146         {
147             enum totalSizeCallback = (size_t size) {
148                 dls.protocol.jsonrpc.send("$/dls.upgradeDls.totalSize", size);
149             };
150             enum chunkSizeCallback = (size_t size) {
151                 dls.protocol.jsonrpc.send("$/dls.upgradeDls.currentSize", size);
152             };
153             enum extractCallback = () {
154                 dls.protocol.jsonrpc.send("$/dls.upgradeDls.extract");
155             };
156 
157             downloadDls(totalSizeCallback, chunkSizeCallback, extractCallback);
158             success = true;
159         }
160         catch (Exception e)
161         {
162             logger.warningf("Could not download DLS: %s", e.message);
163         }
164     }
165 
166     if (!success)
167     {
168         auto dub = new Dub();
169         FetchOptions fetchOpts;
170         fetchOpts |= FetchOptions.forceBranchUpgrade;
171         const pack = dub.fetch("dls", Dependency(">=0.0.0"),
172                 dub.defaultPlacementLocation, fetchOpts);
173 
174         int i;
175         const additionalArgs = [[], ["--force"]];
176 
177         do
178         {
179             try
180             {
181                 buildDls(pack.path.toString().normalized, additionalArgs[i]);
182                 success = true;
183             }
184             catch (UpgradeFailedException e)
185             {
186                 ++i;
187             }
188         }
189         while (i < additionalArgs.length && !success);
190 
191         if (!success)
192         {
193             Util.sendMessage(Util.ShowMessageType.dlsBuildError);
194             return;
195         }
196     }
197 
198     try
199     {
200         linkDls();
201         id = Util.sendMessageRequest(Util.ShowMessageRequestType.showChangelog, [latestVersion]);
202         send(ownerTid(), Util.ThreadMessageData(id,
203                 Util.ShowMessageRequestType.showChangelog, changelogUrl));
204     }
205     catch (FileException e)
206     {
207         Util.sendMessage(Util.ShowMessageType.dlsLinkError);
208     }
209 }
210 
211 @property private string currentVersion()
212 {
213     import std.algorithm : find;
214 
215     const desc = parseJSON(descriptionJson);
216     return desc["packages"].array.find!(p => p["name"] == desc["rootPackage"])[0]["version"].str;
217 }